commit 00694e847287b72d6a9016c0b4b6b28c55565806 Author: Neale Pickett Date: Tue Mar 2 20:45:21 2010 -0700 Initial effort to decouple from buildroot diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f836aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +*.pyc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b282985 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +BASE = /opt/ctf +VAR = $(BASE)/var +WWW = $(BASE)/www +LIB = $(BASE)/lib +BIN = $(BASE)/bin +SBIN = $(BASE)/sbin +BASE_URL = /ctf/ + +install: + id ctf || useradd --system --shell /bin/false --home $(VAR) \ + --comment "Capture The Flag" ctf + install --directory $(LIB) $(BIN) $(SBIN) $(LIB)/disabled + install --directory --owner=ctf $(VAR) + install --directory --owner=ctf $(WWW) + install --directory --owner=ctf $(WWW)/puzzler + install --directory --owner=ctf $(VAR)/points + install --directory --owner=ctf $(VAR)/points/tmp + install --directory --owner=ctf $(VAR)/points/cur + install --directory --owner=ctf $(VAR)/flags + +# Tanks has a lot of files. + install --directory --owner=ctf $(VAR)/tanks + install --directory --owner=ctf $(VAR)/tanks/results + install --directory --owner=ctf $(VAR)/tanks/errors + install --directory --owner=ctf $(VAR)/tanks/ai + install --directory --owner=ctf $(VAR)/tanks/ai/players + install --directory --owner=ctf $(VAR)/tanks/ai/house + + echo 'VAR = "$(VAR)"' > ctf/paths.py + echo 'WWW = "$(WWW)"' >> ctf/paths.py + echo 'LIB = "$(LIB)"' >> ctf/paths.py + echo 'BIN = "$(BIN)"' >> ctf/paths.py + echo 'SBIN = "$(SBIN)"' >> ctf/paths.py + echo 'BASE_URL = "$(BASE_URL)"' >> ctf/paths.py + python setup.py install + + install bin/pointscli $(BIN) + install bin/in.pointsd bin/in.flagd \ + bin/scoreboard bin/run-tanks \ + bin/run-ctf $(SBIN) + cp -r lib/* $(LIB) + cp -r www/* $(WWW) + rm -f $(WWW)/tanks/results + ln -s $(VAR)/tanks/results $(WWW)/tanks/results + cp template.html $(LIB) + + ./mkpuzzles.py --base=$(BASE_URL) --puzzles=puzzles \ + --htmldir=$(WWW)/puzzler --keyfile=$(LIB)/puzzler.keys + + +uninstall: + rm -rf $(VAR) $(WWW) $(LIB) $(BIN) $(SBIN) + rmdir $(BASE) || true diff --git a/bin/badmathbot b/bin/badmathbot new file mode 100755 index 0000000..f6d0075 --- /dev/null +++ b/bin/badmathbot @@ -0,0 +1,265 @@ +#! /usr/bin/python + +import badmath +import time +import os +import traceback +import pickle + +import irc +from ctf import teams +from ctf.flagger import Flagger + +class Gyopi(irc.Bot): + STATE_FN = 'badmath.state' + + SALT = 'this is questionable.' + + MAX_ATTEMPT_RATE = 3 + NOBODY = '\002[nobody]\002' + + def __init__(self, host, channels, dataPath, flagger): + irc.Bot.__init__(self, host, ['gyopi', 'gyopi_', '_gyopi', '[gyopi]'], 'Gyopi', channels) + + self._dataPath = dataPath + + self._flag = flagger + + try: + self._loadState() + except: + self._lvl = 0 + self._flag.set_flag(teams.house) + + self._tokens = [] + 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) + self.write(['TOPIC', '#badmath'], 'type !help') + + 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, 'rb' ) + 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.rename( statePath + '.tmp', statePath) + + def _tellFlag(self, forum): + """Announce who owns the flag.""" + forum.msg('%s has the flag.' % (self._flag.flag)) + + def _tellPuzzle(self, forum): + """Announce the current puzzle.""" + forum.msg('Difficulty level is %d' % self._lvl) + forum.msg('The problem is: %s' % ' '.join( map(str, self._puzzle))) + + def _getStations(self): + stations = {} + file = open(os.path.join(STORAGE, 'stations.txt')) + 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:].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! Order of operations is always left to right in that dimension, but the operators are alien.''') + forum.msg('Order of operations is always left to right.') + #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) + #UN-COMMENT AFTER NMT CTF +# self._lastAttempt[team] = time.time() + 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: That is not correct.' % who) + + # Test a simple one op command. + elif cmd.startswith('?'): + if not args: + forum.msg('%s: Give me an easier problem, and I\'ll ' + 'give you the answer.' % who) + return + + try: + tokens = badmath.parse(''.join(args)) + except (ValueError), 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) + return + + 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 Exception, msg: + forum.msg("%s: That doesn't work at all: %s" % (who, msg)) + + 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('-i', '--irc', dest='ircHost', default='localhost', + help='IRC Host to connect to.') + p.add_option('-f', '--flagd', dest='flagd', default='localhost', + help='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/ctf/badmath', + help='Path to where we can store state info.') + p.add_option('-c', '--channel', dest='channel', default='#badmath', + help='Which channel to join') + + opts, args = p.parse_args() + channels = [opts.channel] + + flagger = Flagger(opts.flagd, opts.password.encode('utf-8')) + gyopi = Gyopi((opts.ircHost, 6667), channels, opts.path, flagger) + irc.run_forever() diff --git a/bin/in.flagd b/bin/in.flagd new file mode 100755 index 0000000..7a16261 --- /dev/null +++ b/bin/in.flagd @@ -0,0 +1,96 @@ +#! /usr/bin/python + +import sys +import optparse +import hmac +import time +import select +from ctf import teams, pointscli +import os +from urllib import quote + +basedir = None +flagsdir = None + +key = 'My First Shared Secret (tm)' +def hexdigest(s): + return hmac.new(key, s.encode('utf-8')).hexdigest() + +def auth(): + # Pretend to be in.tcpmuxd + while True: + line = sys.stdin.readline() + if not line: + return + line = line.strip().lower() + + if line == 'tcpmux': + sys.stdout.write('+Okay, fine.\r\n') + sys.stdout.flush() + continue + elif line == 'help': + sys.stdout.write('tcpmux\r\n') + elif ':::' in line: + # Authentication + cat, passwd = line.split(':::') + if passwd == hexdigest(cat): + return cat + else: + sys.stdout.write('-Blow me.\r\n') + else: + sys.stdout.write('-Blow me.\r\n') + return + +def award(cat, team): + qcat = quote(cat, '') + fn = os.path.join(flagsdir, qcat) + f = open(fn, 'w') + f.write(team) + f.close() + pointscli.award(cat, team, 1) + print('+%s' % team) + sys.stdout.flush() + +def run(): + cat = auth() + if not cat: + return + + now = time.time() + next_award = now - (now % 60) + flag = teams.house + + while True: + now = time.time() + while now >= next_award: + next_award += 60 + award(cat, flag) + + timeout = next_award - now + r, w, x = select.select([sys.stdin], [], [], timeout) + if r: + line = sys.stdin.readline() + if not line: + break + new_flag = line.strip() or teams.house + if new_flag != flag: + # Award a point if the flag is captured + flag = new_flag + award(cat, flag) + +def main(): + p = optparse.OptionParser(usage='%prog [options] FLAGSDIR') + p.add_option('-a', '--auth', dest='cat', default=None, + help='Generate authentication for the given category') + opts, args = p.parse_args() + if opts.cat: + print('%s:::%s' % (opts.cat, hexdigest(opts.cat.encode('utf-8')))) + elif len(args) != 1: + p.error('Wrong number of arguments') + else: + global flagsdir + flagsdir = args[0] + run() + +if __name__ == '__main__': + main() diff --git a/bin/in.heartbeatd b/bin/in.heartbeatd new file mode 100755 index 0000000..3157e96 --- /dev/null +++ b/bin/in.heartbeatd @@ -0,0 +1,5 @@ +#! /bin/sh + +ip=$(echo $UDPREMOTEADDR | cut -d: -f1) +touch $1/$ip +echo 'Hello.' diff --git a/bin/in.pointsd b/bin/in.pointsd new file mode 100755 index 0000000..0b7b7fb --- /dev/null +++ b/bin/in.pointsd @@ -0,0 +1,19 @@ +#! /bin/sh + +## +## This is meant to be run from inotifyd like so: +## +## inotifyd in.pointsd $base/cur:y +## +## inotifyd runs in.pointsd serially, so all we have to do is just echo +## each file to the log, and then remove it. This allows a log message +## to be entered by writing a file into tmp, moving it to cur, and then +## moving along. Even if pointsd dies, everybody is still able to score +## points asynchronously without losing anything: it'll just get picked +## up when pointsd restarts. +## + +# Args: flag dir file +fn=$2/$3 + +cat $fn >> $2/../log && rm $fn diff --git a/bin/kevin b/bin/kevin new file mode 100755 index 0000000..f0e533c --- /dev/null +++ b/bin/kevin @@ -0,0 +1,136 @@ +#! /usr/bin/python + +import os +import optparse +import asynchat +import socket +import asyncore +from urllib import quote_plus as quote + +import irc +from ctf.flagger import Flagger + +nobody = '\002[nobody]\002' + +class Kevin(irc.Bot): + def __init__(self, host, flagger, tokens, victims): + irc.Bot.__init__(self, host, + ['kevin', 'kev', 'kevin_', 'kev_', 'kevinm', 'kevinm_'], + 'Kevin', + ['+kevin']) + self.flagger = flagger + self.tokens = tokens + self.victims = victims + self.affiliation = {} + + def cmd_001(self, sender, forum, addl): + self.write(['OPER', 'bot', 'BottyMcBotpants']) + irc.Bot.cmd_001(self, sender, forum, addl) + + def cmd_JOIN(self, sender, forum, addl): + if sender.name == self.nick: + self.write(['TOPIC', '#badmath'], 'type !help') + self.tell_flag(forum) + + def cmd_381(self, sender, forum, addl): + # You are now an IRC Operator + if self.nick != 'kevin': + self.write(['KILL', 'kevin'], 'You are not kevin. I am kevin.') + self.write(['NICK', 'kevin']) + + 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 tell_flag(self, forum): + forum.msg('%s has the flag.' % (self.flagger.flag or nobody)) + + def cmd_PRIVMSG(self, sender, forum, addl): + text = addl[0] + if text.startswith('!'): + parts = text[1:].split(' ', 1) + cmd = parts[0].lower() + if len(parts) > 1: + args = parts[1] + else: + args = None + if cmd.startswith('r'): + # Register + who = sender.name() + if args: + self.affiliation[who] = args + team = self.affiliation.get(who, nobody) + forum.msg('%s is playing for %s' % (who, team)) + elif cmd.startswith('e'): + # Embrace + forum.ctcp('ACTION', 'hugs %s' % sender.name()) + elif cmd.startswith('f'): + # Flag + self.tell_flag(forum) + elif cmd.startswith('h'): + # Help + forum.msg('Goal: Obtain a token with social engineering.') + forum.msg('Commands: !help, !flag, !register [TEAM], !claim TOKEN, !victims, !embrace') + elif cmd.startswith('c') and args: + # Claim + sn = sender.name() + team = self.affiliation.get(sn) + token = quote(args, safe='') + fn = os.path.join(self.tokens, token) + if not team: + forum.msg('%s: register first (!register TEAM).' % sn) + elif self.flagger.flag == team: + forum.msg('%s: Greedy, greedy.' % sn) + elif not os.path.exists(fn): + forum.msg('%s: Token does not exist (possibly already claimed).' % sn) + else: + os.unlink(fn) + self.flagger.set_flag(team) + self.tell_flag(forum) + elif cmd.startswith('v'): + # Victims + # Open the file each time, so it can change + try: + for line in open(self.victims): + forum.msg(line.strip()) + except IOError: + forum.msg('There are no victims!') + elif cmd == 'traceback': + forum.msg(self.last_tb or 'No traceback') + +def main(): + p = optparse.OptionParser() + p.add_option('-t', '--tokens', dest='tokens', default='./tokens', + help='Directory containing tokens') + p.add_option('-v', '--victims', dest='victims', default='victims.txt', + help='File containing victims information') + p.add_option('-i', '--ircd', dest='ircd', default='localhost', + help='IRC server to connect to') + p.add_option('-f', '--flagd', dest='flagd', default='localhost', + help='Flag server to connect to') + p.add_option('-p', '--password', dest='password', + default='kevin:::7db3e44d53d4a466f8facd7b7e9aa2b7', + help='Flag server password') + p.add_option('-c', '--channel', dest='channel', + help='Channel to join') + opts, args = p.parse_args() + + f = Flagger(opts.flagd, opts.password.encode('utf-8')) + k = Kevin((opts.ircd, 6667), f, opts.tokens, opts.victims) + irc.run_forever() + +if __name__ == '__main__': + main() diff --git a/bin/pointscli b/bin/pointscli new file mode 100755 index 0000000..fecb8ff --- /dev/null +++ b/bin/pointscli @@ -0,0 +1,5 @@ +#! /usr/bin/python + +from ctf import pointscli + +pointscli.main() diff --git a/bin/pollster b/bin/pollster new file mode 100755 index 0000000..5b11c46 --- /dev/null +++ b/bin/pollster @@ -0,0 +1,285 @@ +#!/usr/bin/python + +import os +import re +import sys +import time +import socket +import traceback +import subprocess +import random +import httplib +import optparse +import cStringIO as io + +from ctf import pointscli, html + +ifconfig = '/sbin/ifconfig' +udhcpc = '/sbin/udhcpc' + +class BoundHTTPConnection(httplib.HTTPConnection): + ''' http.client.HTTPConnection doesn't support binding to a particular + address, which is something we need. ''' + + def __init__(self, bindip, host, port=None, strict=None, timeout=None): + httplib.HTTPConnection.__init__(self, host, port, strict) + self.bindip = bindip + self.timeout = timeout + + def connect(self): + ''' Connect to the host and port specified in __init__, but + also bind first. ''' + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.bind((self.bindip, 0)) + self.sock.settimeout(self.timeout) + self.sock.connect((self.host, self.port)) + +def random_mac(): + ''' Set a random mac on the poll interface. ''' + retcode = subprocess.call((ifconfig, opts.iface, 'down')) + mac = ':'.join([opts.mac_vendor] + ['%02x' % random.randint(0,255) for i in range(3)]) + retcode = subprocess.call((ifconfig, opts.iface, 'hw', 'ether', mac, 'up')) + +def dhcp_request(): + ''' Request a new IP on the poll interface. ''' + retcode = subprocess.call((udhcpc, '-i', opts.iface, '-q')) + +def get_ip(): + ''' Return the IP of the poll interface. ''' + ip_match = re.compile(r'inet addr:([0-9.]+)') + p = subprocess.Popen((ifconfig, opts.iface), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = p.communicate() + for line in out.splitlines(): + m = ip_match.search(line) + if m is not None: + return m.group(1).decode('utf-8') + return '10.1.1.1' + +def socket_poll(srcip, ip, port, msg, prot, max_recv=1): + ''' Connect via socket to the specified : using the + specified , send the specified and return the + response or None if something went wrong. specifies + how many times to read from the socket (defaults to once). ''' + + # create a socket + try: + sock = socket.socket(socket.AF_INET, prot) + except Exception, e: + print('pollster: create socket failed (%s)' % e) + traceback.print_exc() + return None + + sock.bind((srcip, 0)) + sock.settimeout(opts.timeout) + + # connect + try: + sock.connect((ip, port)) + except socket.timeout, e: + print('pollster: attempt to connect to %s:%d timed out (%s)' % (ip, port, e)) + traceback.print_exc() + return None + except Exception, e: + print('pollster: attempt to connect to %s:%d failed (%s)' % (ip, port, e)) + traceback.print_exc() + return None + + # send something + sock.send(msg) + + # get a response + resp = [] + try: + # read from the socket until responses or read, + # a timeout occurs, the socket closes, or some other exception + # is raised + for i in range(max_recv): + data = sock.recv(1024) + if len(data) == 0: + break + resp.append(data) + + except socket.timeout, e: + print('pollster: timed out waiting for a response from %s:%d (%s)' % (ip, port, e)) + traceback.print_exc() + except Exception, e: + print('pollster: receive from %s:%d failed (%s)' % (ip, port, e)) + traceback.print_exc() + + sock.close() + + if len(resp) == 0: + return None + + return ''.join(resp) + +# PUT POLLS FUNCTIONS HERE +# Each function should take an IP address and return a team name or None +# if (a) the service is not up, (b) it doesn't return a valid team name. + +def poll_fingerd(srcip, ip): + ''' Poll the fingerd service. Returns None or a team name. ''' + resp = socket_poll(srcip, ip, 79, 'flag\n', socket.SOCK_STREAM) + if resp is None: + return None + return resp.split('\n')[0] + +def poll_noted(srcip, ip): + ''' Poll the noted service. Returns None or a team name. ''' + resp = socket_poll(srcip, ip, 4000, 'rflag\n', socket.SOCK_STREAM) + if resp is None: + return None + return resp.split('\n')[0] + +def poll_catcgi(srcip, ip): + ''' Poll the cat.cgi web service. Returns None or a team name. ''' + + try: + conn = BoundHTTPConnection(srcip, ip, timeout=opts.timeout) + conn.request('GET', '/var/www/flag') + except Exception, e: + traceback.print_exc() + return None + + resp = conn.getresponse() + if resp.status != 200: + conn.close() + return None + + data = resp.read() + conn.close() + return data.split('\n')[0] + +def poll_tftpd(srcip, ip): + ''' Poll the tftp service. Returns None or a team name. ''' + resp = socket_poll(srcip, ip, 69, '\x00\x01' + 'flag' + '\x00' + 'octet' + '\x00', socket.SOCK_DGRAM) + if resp is None: + return None + + if len(resp) <= 5: + return None + + resp = resp.split('\n')[0] + + # ack + _ = socket_poll(srcip, ip, 69, '\x00\x04\x00\x01' + '\x00' * 14, socket.SOCK_DGRAM, 0) + + return resp[4:].split('\n')[0] + +# PUT POLL FUNCTIONS IN HERE OR THEY WONT BE POLLED +POLLS = { + 'fingerd' : poll_fingerd, + 'noted' : poll_noted, + 'catcgi' : poll_catcgi, + 'tftpd' : poll_tftpd, +} + + + +p = optparse.OptionParser() +p.add_option('-d', '--debug', action='store_true', dest='debug', + default=False, + help='Turn on debugging output') +p.add_option('-s', '--interval', type='float', dest='interval', + default=60, + help='Time between polls, in seconds (default: %default)') +p.add_option('-t', '--timeout', type='float', dest='timeout', + default=0.5, + help='Poll timeout, in seconds (default: %default)') +p.add_option('-b', '--heartbeat-dir', dest='heartbeat_dir', + default='/var/lib/ctf/heartbeat', + help='Where in.heartbeatd writes its files') +p.add_option('-r', '--results-page', dest='results', + default='/tmp/services.html', + help='Where to write results file') +p.add_option('-i', '--interface', dest='iface', + default='eth1', + help='Interface to bind to') +p.add_option('-m', '--mac-vendor', dest='mac_vendor', + default='00:01:c0', + help='MAC vendor to look for (default: %default)') +opts, args = p.parse_args() + +socket.setdefaulttimeout(opts.timeout) + +ip_re = re.compile('(\d{1,3}\.){3}\d{1,3}') +# loop forever +while True: + + random_mac() + dhcp_request() + + srcip = get_ip() + + t_start = time.time() + + # gather the list of IPs to poll + ips = os.listdir(opts.heartbeat_dir) + + out = io.StringIO() + for ip in ips: + # check file name format is ip + if ip_re.match(ip) is None: + continue + + # remove the file + fn = os.path.join(opts.heartbeat_dir, ip) + try: + os.remove(fn) + except Exception, e: + print('pollster: could not remove %s' % fn) + traceback.print_exc() + + results = {} + + if opts.debug: + print('ip: %s' % ip) + + if out is not None: + out.write('

%s

\n' % ip) + out.write('\n') + out.write('\n') + + # perform polls + for service,func in POLLS.items(): + try: + team = func(srcip, ip).decode('utf-8') + if len(team) == 0: + team = 'dirtbags' + except: + team = 'dirtbags' + + if opts.debug: + print('\t%s - %s' % (service, team)) + + if out is not None: + out.write('\n' % (service, team)) + + pointscli.award('svc.' + service, team, 1) + + if out is not None: + out.write('
Service NameFlag Holder
%s%s
\n') + + if opts.debug: + print('+-----------------------------------------+') + + time_str = time.strftime('%a, %d %b %Y %H:%M:%S %Z') + out.write(''' +

This page was generated on %s. That was ? seconds ago.

+ + ''' % (time_str, time.time()*1000)) + + t_end = time.time() + exec_time = int(t_end - t_start) + sleep_time = opts.interval - exec_time + + html.write(opts.results,'Team Service Availability', out.getvalue()) + + # sleep until its time to poll again + time.sleep(sleep_time) + diff --git a/bin/run-ctf b/bin/run-ctf new file mode 100755 index 0000000..54be56e --- /dev/null +++ b/bin/run-ctf @@ -0,0 +1,33 @@ +#! /bin/sh + +POINTS=var/points/log + +cd $(dirname $0)/.. + +while true; do + # Timestamp + start=$(date +%s) + next=$(expr $start + 60) + + # If enabled, run tanks + if ! [ -f var/disabled/tanks ]; then + sbin/run-tanks -1 var/tanks + fi + + # Collect any new points + for fn in var/points/cur/*; do + [ -f $fn ] || continue + cat $fn >> $POINTS || break + rm $fn + done + + if [ -f $POINTS ]; then + sbin/scoreboard -t www/scoreboard.html -j www/myplot.js < $POINTS + fi + + # Wait until the next minute + now=$(date +%s) + if [ $now -lt $next ]; then + sleep $(expr $next - $now) + fi +done \ No newline at end of file diff --git a/bin/run-tanks b/bin/run-tanks new file mode 100755 index 0000000..e07a49c --- /dev/null +++ b/bin/run-tanks @@ -0,0 +1,59 @@ +#! /usr/bin/python + +import optparse +import os +import shutil +import socket +import time +from ctf import pointscli, teams, paths +from tanks import Pflanzarr + +MAX_HIST = 30 +HIST_STEP = 100 + +running = True + +def run_tanks(basedir, turns): + try: + p = Pflanzarr.Pflanzarr(basedir) + p.run(turns) + winner = p.winner + except Pflanzarr.NotEnoughPlayers: + winner = teams.house + pointscli.award('tanks', winner, 1) + + winnerFile = open(os.path.join(basedir, 'winner'),'w') + winnerFile.write(winner or teams.house) + winnerFile.close() + + # Fake being a flag, so the most recent winner shows up on the + # scoreboard. + try: + open(os.path.join(paths.VAR, 'flags', 'tanks'), 'w').write(winner or teams.house) + except IOError: + pass + + +def main(): + parser = optparse.OptionParser('%prog [options] DATA_DIR') + parser.add_option('-1', '--once', + action='store_true', dest='once', + help='Run only once') + parser.add_option('-t', '--max-turns', + type='int', dest='turns', default=500, + help='Maximum number of turns per round') + parser.add_option('-s', '--sleep-time', + type='int', dest='sleep', default=60, + help='Wait SLEEP seconds between turns (default %default)') + opts, args = parser.parse_args() + if (len(args) != 1): + parser.error('Wrong number of arguments') + + while running: + run_tanks(args[0], opts.turns) + if opts.once: + break + time.sleep(opts.sleep) + +if __name__ == '__main__': + main() diff --git a/bin/scoreboard b/bin/scoreboard new file mode 100755 index 0000000..e2654f6 --- /dev/null +++ b/bin/scoreboard @@ -0,0 +1,174 @@ +#! /usr/bin/python + +import sys +import codecs +import time +import optparse +import string +import os +from urllib import unquote +from ctf import teams, html, paths +from codecs import open +from sets import Set as set +from cgi import escape + +flags_dir = os.path.join(paths.VAR, 'flags') +sys.stdin = codecs.getreader('utf-8')(sys.stdin) + +def incdict(dict, key, amt=1): + dict[key] = dict.get(key, 0) + amt + +class Chart: + def __init__(self): + self.points_by_cat = {} + self.points_by_cat_team = {} + self.high_score = 0.001 + self.teams = set() + self.cats = set() + self.log = [] + + def add_points(self, when, cat, team, points): + self.log.append((when, cat, team, points)) + self.teams.add(team) + self.cats.add(cat) + incdict(self.points_by_cat, cat, points) + incdict(self.points_by_cat_team, (cat, team), points) + + def team_points(self, team): + points = 0 + for cat, tot in self.points_by_cat.items(): + if not tot: + continue + team_points = self.team_points_in_cat(cat, team) + points += team_points / float(tot) + return points + + def team_points_in_cat(self, cat, team): + return self.points_by_cat_team.get((cat, team), 0) + + def write_js(self, f): + start = self.log[0][0] + end = self.log[-1][0] + + # Calculate high score + high_score = reduce(max, [self.team_points(t) for t in self.teams]) + + width = end - start + height = high_score * 1.1 + + f.write('function draw(id) {\n') + f.write(' p = new Plot(id, %d, %.3f);\n' % (width, height)) + for team in self.teams: + f.write(' p.line("#%s",[' % teams.color(team)) + score = 0 + for when, cat, t, points in self.log: + if t == team: + cat_points = self.points_by_cat[cat] + if not cat_points: + continue + pct = float(points) / cat_points + score += pct + f.write('[%d,%.2f],' % (when - start, score)) + f.write(']); // %s\n' % team) + f.write('}') + + def make_table(self): + body = [] + body.append('') + body.append('') + body.append('') + for cat in self.cats: + points = self.points_by_cat[cat] + if not points: + continue + body.append('') + body.append('') + + body.append('') + body.append('') + for cat in self.cats: + total = self.points_by_cat[cat] + if not total: + continue + body.append('') + body.append('') + body.append('
Overall') + body.append(' %s (%d)' % (cat, points)) + try: + fn = os.path.join(flags_dir, cat) + team = open(fn).read().strip() or teams.house + body.append('
') + body.append(' ' % teams.color(team)) + body.append(' %s\n' % (cat, escape(team[:15]))) + body.append(' ') + except IOError: + pass + body.append('
    ') + totals = [] + for team in self.teams: + total = self.team_points(team) + totals.append((total, team)) + totals.sort() + totals.reverse() + for total, team in totals: + if total < 0.1: + break + body.append('
  1. %s (%0.3f)
  2. ' + % (teams.color(team), escape(team[:15]), total)) + body.append('
') + scores = sorted([(self.team_points_in_cat(cat, team), team) for team in self.teams]) + for score, team in scores: + if not score: + continue + color = teams.color(team) + body.append('
' % (float(score * 100)/total, color)) + body.append(' %s: %d' % (cat, escape(team[:15]), score)) + body.append('
') + body.append('
') + + return '\n'.join(body) + +def main(): + p = optparse.OptionParser(usage='%prog [options] < logfile') + p.add_option('-t', '--html', dest='html', default=None, + help='Write a web page to HTML') + p.add_option('-j', '--javascript', dest='js', default=None, + help='Write javascript params to JS') + + opts, args = p.parse_args() + if args: + return p.print_help() + + chart = Chart() + for line in sys.stdin: + line = line.strip() + try: + date, qcat, qteam, points = line.split('\t') + except ValueError: + print 'Possible line corruption: %s' % (repr(line)[:40]) + cat = unquote(qcat) + team = unquote(qteam) + when = time.strptime(date, '%Y-%m-%dT%H:%M:%S') + chart.add_points(time.mktime(when), + cat, + team, + int(points)) + + if opts.html: + hdr = ('' + '' + '') + body = chart.make_table() + body += '\n' + html.write(opts.html, + 'Scoreboard', + body, + hdr=hdr, + body_class='wide', + onload="draw('history')") + if opts.js: + f = open(opts.js, 'w', encoding='utf-8') + chart.write_js(f) + +if __name__ == '__main__': + main() diff --git a/build/lib/ctf/__init__.py b/build/lib/ctf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/ctf/flagger.py b/build/lib/ctf/flagger.py new file mode 100644 index 0000000..04a9fa8 --- /dev/null +++ b/build/lib/ctf/flagger.py @@ -0,0 +1,33 @@ +#! /usr/bin/python + +import asynchat +import asyncore +import socket + +class Flagger(asynchat.async_chat): + """Use to connect to flagd and submit the current flag holder.""" + + def __init__(self, addr, auth): + asynchat.async_chat.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((addr, 1)) + self.push(auth + '\n') + self.flag = None + + def handle_read(self): + # We don't care. + msg = self.recv(4096) + + 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 = '' + self.push(eteam + '\n') + self.flag = team diff --git a/build/lib/ctf/html.py b/build/lib/ctf/html.py new file mode 100644 index 0000000..101c4d7 --- /dev/null +++ b/build/lib/ctf/html.py @@ -0,0 +1,35 @@ +#! /usr/bin/python + +import os +import string +import sys +from codecs import open + +from paths import * + +template_fn = os.path.join(LIB, 'template.html') +template = string.Template(open(template_fn, encoding='utf-8').read()) + +base = BASE_URL +css = base + 'ctf.css' + +def substitute(title, body, base=base, hdr='', body_class='', onload='', links=''): + return template.substitute(title=title, + hdr=hdr, + body_class=body_class, + base=base, + links=links, + onload=onload, + body=body) + +def serve(title, body, **kwargs): + out = substitute(title, body, **kwargs) + print 'Content-type: text/html' + print 'Content-length: %d' % len(out) + print + sys.stdout.write(out) + sys.stdout.flush() + +def write(filename, title, body, **kwargs): + f = open(filename, 'w', encoding='utf-8') + f.write(substitute(title, body, **kwargs)) diff --git a/build/lib/ctf/paths.py b/build/lib/ctf/paths.py new file mode 100644 index 0000000..336c75c --- /dev/null +++ b/build/lib/ctf/paths.py @@ -0,0 +1,6 @@ +VAR = "/opt/ctf/var" +WWW = "/opt/ctf/www" +LIB = "/opt/ctf/lib" +BIN = "/opt/ctf/bin" +SBIN = "/opt/ctf/sbin" +BASE_URL = "/ctf/" diff --git a/build/lib/ctf/pointscli.py b/build/lib/ctf/pointscli.py new file mode 100644 index 0000000..5671ebc --- /dev/null +++ b/build/lib/ctf/pointscli.py @@ -0,0 +1,40 @@ +#! /usr/bin/python + +from urllib import quote +import teams +import time +import os +import paths + +pointsdir = os.path.join(paths.VAR, 'points') + +def award(cat, team, points): + if not team: + team = teams.house + now = time.strftime('%Y-%m-%dT%H:%M:%S') + pid = os.getpid() + qcat = quote(cat, '') + qteam = quote(team, '') + basename = '%s.%d.%s.%s' % (now, pid, qcat, qteam) + # FAT can't handle : + basename = basename.replace(':', '.') + tmpfn = os.path.join(pointsdir, 'tmp', basename) + curfn = os.path.join(pointsdir, 'cur', basename) + f = open(tmpfn, 'w') + f.write('%s\t%s\t%s\t%d\n' % (now, cat, team, points)) + f.close() + os.rename(tmpfn, curfn) + +def main(): + import optparse + + p = optparse.OptionParser('%prog CATEGORY TEAM POINTS') + opts, args = p.parse_args() + if len(args) != 3: + p.error('Wrong number of arguments') + cat, team, points = args + points = int(points) + award(cat, team, points) + +if __name__ == '__main__': + main() diff --git a/build/lib/ctf/teams.py b/build/lib/ctf/teams.py new file mode 100644 index 0000000..3e9407c --- /dev/null +++ b/build/lib/ctf/teams.py @@ -0,0 +1,72 @@ +#! /usr/bin/python + +import fcntl +import time +import os +from urllib import quote, unquote +import paths + +house = 'dirtbags' +passwdfn = os.path.join(paths.VAR, 'passwd') +team_colors = ['F0888A', '88BDF0', '00782B', '999900', 'EF9C00', + 'F4B5B7', 'E2EFFB', '89CA9D', 'FAF519', 'FFE7BB', + 'BA88F0', '8DCFF4', 'BEDFC4', 'FFFAB2', 'D7D7D7', + 'C5B9D7', '006189', '8DCB41', 'FFCC00', '898989'] + +teams = {} +built = 0 +def build_teams(): + global teams, built + if not os.path.exists(passwdfn): + return + if os.path.getmtime(passwdfn) <= built: + return + + teams = {} + try: + f = open(passwdfn) + for line in f: + line = line.strip() + if not line: + continue + team, passwd, color = map(unquote, line.strip().split('\t')) + teams[team] = (passwd, color) + except IOError: + pass + built = time.time() + +def validate(team): + build_teams() + +def chkpasswd(team, passwd): + validate(team) + if teams.get(team, [None, None])[0] == passwd: + return True + else: + return False + +def exists(team): + validate(team) + if team == house: + return True + return team in teams + +def add(team, passwd): + build_teams() + color = team_colors[len(teams)%len(team_colors)] + + assert team not in teams, "Team already exists." + + f = open(passwdfn, 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.seek(0, 2) + f.write('%s\t%s\t%s\n' % (quote(team, ''), + quote(passwd, ''), + quote(color, ''))) + +def color(team): + validate(team) + t = teams.get(team) + if not t: + return '888888' + return t[1] diff --git a/build/lib/tanks/Function.py b/build/lib/tanks/Function.py new file mode 100644 index 0000000..406e564 --- /dev/null +++ b/build/lib/tanks/Function.py @@ -0,0 +1,46 @@ +import math + +class Function(object): + """Represents a single condition or action. This doc string is printed + as user documentation. You should override it to say something useful.""" + + def __call__(self, tank): + """The __call__ method should be of this basic form. Actions + should return None, conditions should return True or False. Actions + should utilize the set* methods of tanks. Conditions can utilize the + tanks get* methods.""" + pass + + def _limitArgs(self, args, max): + """Raises a ValueError if there are more than max args.""" + if len(args) > max: + raise ValueError("Too many arguments: %s" % ','.join(args)) + + def _checkRange(self, value, name, min=0, max=100): + """Check that the value is in the given range. + Raises an exception with useful info for invalid values. Name is used to + let the user know which value is wrong.""" + try: + value = int(value) + except: + raise ValueError("Invalid %s value: %s" % (name, value)) + assert value >= min and value <= max, "Invalid %s. %ss must be in"\ + " the %s %d-%d" % \ + (name, name.capitalize(), value, min, max) + + return value + + def _convertAngle(self, value, name): + """Parse the given value as an angle in degrees, and return its value + in radians. Raise useful errors. + Name is used in the errors to describe the field.""" + try: + angle = math.radians(value) + except: + raise ValueError("Invalid %s value: %s" % (name, value)) + + assert angle >= 0 and angle < 2*math.pi, "Invalid %s; "\ + "It be in the range 0 and 359." % name + + return angle + diff --git a/build/lib/tanks/GameMath.py b/build/lib/tanks/GameMath.py new file mode 100644 index 0000000..481bf81 --- /dev/null +++ b/build/lib/tanks/GameMath.py @@ -0,0 +1,206 @@ +import math + +def rotatePoint(point, angle): + """Assuming 0,0 is the center, rotate the given point around it.""" + + x,y = point + r = math.sqrt(x**2 + y**2) + if r == 0: + return 0, 0 + + theta = math.acos(x/r) + if y < 0: + theta = -theta + theta = theta + angle + return int(round(r*math.cos(theta))), int(round(r*math.sin(theta))) + +def rotatePoly(points, angle): + """Rotate the given list of points around 0,0 by angle.""" + return [ rotatePoint(point, angle) for point in points ] + +def displace(point, disp, limits): + """Displace point by disp, wrapping around limits.""" + x = (point[0] + disp[0]) + while x >= limits[0]: + x = x - limits[0] + while x < 0: + x = x + limits[0] + + y = (point[1] + disp[1]) + while y >= limits[1]: + y = y - limits[1] + while y < 0: + y = y + limits[1] + + return x,y + +def displacePoly(points, disp, limits, coordSequence=False): + """Displace each point (x,y) in 'points' by 'disp' (x,y). The limits of + the drawing space are assumed to be at x=0, y=0 and x=limits[0], + y=limits[1]. If the poly overlaps the edge of the drawing space, the + poly is duplicated on each side. +@param coordSequence: If true, the coordinates are returned as a sequence - + x1, y1, x2, y2, ... This is need by some PIL drawing + commands. +@returns: A list of polys, displaced by disp + """ + xDup = 0; yDup = 0 + maxX, maxY = limits + basePoints = [] + for point in points: + x,y = int(point[0] + disp[0]), int(point[1] + disp[1]) + + # Check if duplication is needed on each axis + if x > maxX: + # If this is negative, then we need to duplicate in the negative + # direction. + xDup = -1 + elif x < 0: + xDup = 1 + + if y > maxY: + yDup = -1 + elif y < 0: + yDup = 1 + + basePoints.append( (x,y) ) + + polys = [basePoints] + if xDup: + polys.append([(x + maxX*xDup, y) for x,y in basePoints] ) + if yDup: + polys.append([(x, maxY*yDup + y) for x,y in basePoints] ) + if xDup and yDup: + polys.append([(x+maxX*xDup, maxY*yDup+y) for x,y in basePoints]) + + # Switch coordinates to sequence mode. + # (x1, y1, x2, y2) instead of ((x1, y1), (x2, y2)) + if coordSequence: + seqPolys = [] + for poly in polys: + points = [] + for point in poly: + points.extend(point) + seqPolys.append(points) + polys = seqPolys + + return polys + +def polar2cart(r, theta): + """Return the cartesian coordinates for r, theta.""" + x = r*math.cos(theta) + y = r*math.sin(theta) + return x,y + +def minShift(center, point, limits): + """Get the minimum distances between the two points, given that the board + wraps at the givin limits.""" + dx = point[0] - center[0] + if dx < -limits[0]/2.0: + dx = point[0] + limits[0] - center[0] + elif dx > limits[0]/2.0: + dx = point[0] - (center[0] + limits[0]) + + dy = point[1] - center[1] + if dy < - limits[1]/2.0: + dy = point[1] + limits[1] - center[1] + elif dy > limits[1]/2.0: + dy = point[1] - (limits[1] + center[1]) + + return dx, dy + +def relativePolar(center, point, limits): + """Returns the angle, from zero, to the given point assuming this +center is the origin. Take into account wrapping round the limits of the board. +@returns: r, theta + """ + + dx, dy = minShift(center, point, limits) + + r = math.sqrt(dx**2 + dy**2) + theta = math.acos(dx/r) + if dy < 0: + theta = 2*math.pi - theta + + return r, theta + +def reduceAngle(angle): + """Reduce the angle such that it is in 0 <= angle < 2pi""" + + while angle >= math.pi*2: + angle = angle - math.pi*2 + while angle < 0: + angle = angle + math.pi*2 + + return angle + +def angleDiff(angle1, angle2): + """Returns the difference between the two angles. They are assumed +to be in radians, and must be in the range 0 <= angle < 2*pi. +@raises AssertionError: The angles given must be in the range 0 <= angle < 2pi +@returns: The minimum distance between the two angles; The distance + is negative if angle2 leads angle1 (clockwise).. + """ + + for angle in angle1, angle2: + assert angle < 2*math.pi and angle >= 0, \ + 'angleDiff: bad angle %s' % angle + + diff = angle2 - angle1 + if diff > math.pi: + diff = diff - 2*math.pi + elif diff < -math.pi: + diff = diff + 2*math.pi + + return diff + +def getDist(point1, point2): + """Returns the distance between point1 and point2.""" + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + + return math.sqrt(dx**2 + dy**2) + +def segmentCircleCollision(segment, center, radius): + """Return True if the given circle touches the given line segment. +@param segment: A list of two points [(x1,y1), (x2, y2)] that define + the line segment. +@param center: The center point of the circle. +@param radius: The radius of the circle. +@returns: True if the the circle touches the line segment, False otherwise. + """ + + a = getDist(segment[0], center) + c = getDist(segment[1], center) + base = getDist(segment[0], segment[1]) + + # If we're close enough to the end points, then we're close + # enough to the segment. + if a < radius or c < radius: + return True + + # First we find the are of the triangle formed by the line segment + # and point. I use Heron's formula for the area. Using this, we'll + # find the distance d from the point to the line. We'll later make + # sure that the collision is with the line segment, and not just the + # line. + s = (a + c + base)/2 + A = math.sqrt(s*(s - a)*(s - c)*(s - base)) + d = 2*A/base + +# print s, a, c, A, d, radius + + # If the distance from the point to the line is more than the + # target radius, this isn't a hit. + if d > radius: + return False + + # If the distance from an endpoint to the intersection between + # our line segment and the line perpendicular to it that passes through + # the point is longer than the line segment, then this isn't a hit. + elif math.sqrt(a**2 - d**2) > base or \ + math.sqrt(c**2 - d**2) > base: + return False + else: + # The triangle is acute, that means we're close enough. + return True diff --git a/build/lib/tanks/Pflanzarr.py b/build/lib/tanks/Pflanzarr.py new file mode 100644 index 0000000..18a54fe --- /dev/null +++ b/build/lib/tanks/Pflanzarr.py @@ -0,0 +1,399 @@ +import fcntl +import math +import os +import random +import cgi +from sets import Set as set +from ctf import teams, html, paths +from cStringIO import StringIO + +from urllib import unquote, quote + +import Tank + +class NotEnoughPlayers(Exception): + pass + +class Pflanzarr: + SPACING = 150 + + def __init__(self, dir): + """Initialize a new game of Pflanzarr. +@param dir: The data directory.""" + + # Setup the game environment + self._setupDirectories(dir) + + # Figure out what game number this is. + self.gameNum = self._getGameNum() + self.gameFilename = os.path.join(self._resultsDir, '%04d.html' % self.gameNum) + + tmpPlayers = os.listdir(self._playerDir) + players = [] + for p in tmpPlayers: + p = unquote(p) + if (not (p.startswith('.') + or p.endswith('#') + or p.endswith('~')) + and teams.exists(p)): + players.append(p) + + AIs = {} + for player in players: + AIs[player] = open(os.path.join(self._playerDir, player)).read() + defaultAIs = self._getDefaultAIs(dir) + + if len(players) < 1: + raise NotEnoughPlayers() + + # The one is added to ensure that there is at least one house + # bot. + cols = math.sqrt(len(players) + 1) + if int(cols) != cols: + cols = cols + 1 + + cols = int(cols) + cols = max(cols, 2) + + rows = len(players)/cols + if len(players) % cols != 0: + rows = rows + 1 + rows = max(rows, 2) + + self._board = (cols*self.SPACING, rows*self.SPACING) + + while len(players) < cols*rows: + players.append(None) + + self._tanks = [] + for i in range(cols): + for j in range(rows): + startX = i*self.SPACING + self.SPACING/2 + startY = j*self.SPACING + self.SPACING/2 + player = random.choice(players) + players.remove(player) + color = '#' + teams.color(player) + tank = Tank.Tank( player, (startX, startY), color, + self._board, testMode=True) + if player == None: + tank.program(random.choice(defaultAIs)) + else: + tank.program(AIs[player]) + self._tanks.append(tank) + + # We only want to make these once, so we do it here. + self._tanksByX = list(self._tanks) + self._tanksByY = list(self._tanks) + + self._deadTanks = set() + + def run(self, maxTurns=None): + kills = {} + for tank in self._tanks: + kills[tank] = set() + + # Open HTML output + hdr = StringIO() + hdr.write('\n' + '\n') + + # Decide on the winner + winner = self._chooseWinner(kills) + self.winner = winner.name + + # Now generate HTML body + body = StringIO() + body.write(' \n' % self._board) + body.write(' Sorry, you need an HTML5-capable browser to see this.\n' + ' \n' + '

\n') + if self.gameNum > 0: + body.write(' ← Prev |' % + (self.gameNum - 1)) + body.write(' Next → |' % + (self.gameNum + 1)) + body.write(' 0 fps\n' + '

\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n') + + tanks = self._tanks[:] + tanks.remove(winner) + tanks[0:0] = [winner] + for tank in tanks: + if tank is winner: + rowStyle = ('style="font-weight: bold; ' + 'color: #000; ' + 'background-color: %s;"' % tank.color) + else: + rowStyle = 'style="background-color:%s; color: #000;"' % tank.color + if tank.name: + name = cgi.escape(tank.name) + else: + name = teams.house + body.write('' % + (rowStyle, + name, + len(kills[tank]), + cgi.escape(tank.deathReason))) + body.write('
TeamKillsCause of Death
%s%d%s
\n') + + # Write everything out + html.write(self.gameFilename, + 'Tanks round %d' % self.gameNum, + body.getvalue(), + hdr=hdr.getvalue(), + onload='start(turns);') + + + + def _killTanks(self, tanks, reason): + for tank in tanks: + if tank in self._tanksByX: + self._tanksByX.remove(tank) + if tank in self._tanksByY: + self._tanksByY.remove(tank) + + tank.die(reason) + + self._deadTanks = self._deadTanks.union(tanks) + + def _chooseWinner(self, kills): + """Choose a winner. In case of a tie, live tanks prevail, in case + of further ties, a winner is chosen at random. This outputs the winner + to the winners file and outputs a results table html file.""" + tanks = list(self._tanks) + def winSort(t1, t2): + """Sort by # of kill first, then by life status.""" + result = cmp(len(kills[t1]), len(kills[t2])) + if result != 0: + return result + + if t1.isDead and not t2.isDead: + return -1 + elif not t1.isDead and t2.isDead: + return 1 + else: + return 0 + tanks.sort(winSort) + tanks.reverse() + + # Get the list of potential winners + winners = [] + for i in range(len(tanks)): + if len( kills[tanks[0]] ) == len( kills[tanks[i]] ) and \ + tanks[0].isDead == tanks[i].isDead: + winners.append(tanks[i]) + else: + break + winner = random.choice(winners) + return winner + + + def _outputErrors(self, tank): + """Output errors for each team.""" + if tank.name == None: + return + + if tank._program.errors: + print tank.name, 'has errors' + + + fileName = os.path.join(self._errorDir, quote(tank.name, '')) + file = open(fileName, 'w') + for error in tank._program.errors: + file.write(error) + file.write('\n') + file.close() + + def _getNear(self): + """A dictionary of the set of tanks nearby each tank. Nearby is + defined as within the square centered the tank with side length equal + twice the sensor range. Only a few tanks within the set (those in the + corners of the square) should be outside the sensor range.""" + + self._tanksByX.sort(lambda t1, t2: cmp(t1.pos[0], t2.pos[0])) + self._tanksByY.sort(lambda t1, t2: cmp(t1.pos[1], t2.pos[1])) + + nearX = {} + nearY = {} + for tank in self._tanksByX: + nearX[tank] = set() + nearY[tank] = set() + + numTanks = len(self._tanksByX) + offset = 1 + for index in range(numTanks): + cTank = self._tanksByX[index] + maxRange = cTank.SENSOR_RANGE + cTank.RADIUS + 1 + near = set([cTank]) + for i in [(j + index) % numTanks for j in range(1, offset)]: + near.add(self._tanksByX[i]) + while offset < numTanks: + nTank = self._tanksByX[(index + offset) % numTanks] + if (index + offset >= numTanks and + self._board[0] + nTank.pos[0] - cTank.pos[0] < maxRange): + near.add(nTank) + offset = offset + 1 + elif (index + offset < numTanks and + nTank.pos[0] - cTank.pos[0] < maxRange ): + near.add(nTank) + offset = offset + 1 + else: + break + + if offset > 1: + offset = offset - 1 + + for tank in near: + nearX[tank] = nearX[tank].union(near) + + offset = 1 + for index in range(numTanks): + cTank = self._tanksByY[index] + maxRange = cTank.SENSOR_RANGE + cTank.RADIUS + 1 + near = set([cTank]) + for i in [(j + index) % numTanks for j in range(1, offset)]: + near.add(self._tanksByY[i]) + while offset < numTanks: + nTank = self._tanksByY[(index + offset) % numTanks] + if (index + offset < numTanks and + nTank.pos[1] - cTank.pos[1] < maxRange): + near.add(nTank) + offset = offset + 1 + elif (index + offset >= numTanks and + self._board[1] + nTank.pos[1] - cTank.pos[1] < maxRange): + near.add(nTank) + offset = offset + 1 + else: + break + + if offset > 1: + offset = offset - 1 + + for tank in near: + nearY[tank] = nearY[tank].union(near) + + near = {} + for tank in self._tanksByX: + near[tank] = nearX[tank].intersection(nearY[tank]) + near[tank].remove(tank) + + return near + + def _setupDirectories(self, dir): + """Setup all the directories needed by the game.""" + + if not os.path.exists(dir): + os.mkdir(dir) + + self._dir = dir + + # Don't run more than one game at the same time. + self._lockFile = open(os.path.join(dir, '.lock'), 'a') + try: + fcntl.flock(self._lockFile, fcntl.LOCK_EX|fcntl.LOCK_NB) + except: + sys.exit(1) + + # Setup all the directories we'll need. + self._resultsDir = os.path.join(dir, 'results') + self._errorDir = os.path.join(dir, 'errors') + self._playerDir = os.path.join(dir, 'ai', 'players') + + def _getDefaultAIs(self, basedir): + """Load all the house bot AIs.""" + defaultAIs = [] + + path = os.path.join(basedir, 'ai', 'house') + files = os.listdir(path) + for fn in files: + if fn.startswith('.'): + continue + + fn = os.path.join(path, fn) + file = open(fn) + defaultAIs.append(file.read()) + + return defaultAIs + + def _getGameNum(self): + """Figure out what game number this is from the past games played.""" + + games = os.listdir(self._resultsDir) + games.sort() + if games: + fn = games[-1] + s, _ = os.path.splitext(fn) + return int(s) + 1 + else: + return 0 + +if __name__ == '__main__': + import sys, traceback + try: + p = Pflanzarr(sys.argv[1]) + p.run(int(sys.argv[3])) + except: + traceback.print_exc() + print "Usage: Pflanzarr.py dataDirectory #turns" + + diff --git a/build/lib/tanks/Program.py b/build/lib/tanks/Program.py new file mode 100644 index 0000000..cab13ab --- /dev/null +++ b/build/lib/tanks/Program.py @@ -0,0 +1,234 @@ +"""

Introduction

+You are the proud new operator of a M-375 Pflanzarr Tank. Your tank is +equipped with a powerful laser cannon, independently rotating turret +section, up to 10 enemy detection sensors, and a standard issue NATO hull. +Unfortunately, it lacks seats, and thus must rely own its own wits and your +skills at designing those wits to survive. + +

Programming Your Tank

+Your tanks are programmed using the Super Useful Command and Kontrol language, +the very best in laser tank AI languages. It includes amazing features such +as comments (Started by a #, ended at EOL), logic, versatility, and +semi-colons (all lines must end in one). As with all new military systems +it utilizes only integers; we must never rest in our +diligence against the communist floating point conspiracy. Whitespace is +provided by trusted contractors, and should never interfere with operations. +

+Your program should be separated into Setup and AI commands. The definitions +section lets you designated the behaviors of its sensors and memory. +Each setup command must begin with a '>'. Placing setup commands after +the first AI command is a violation of protocol. +Here are some examples of correct setup commands: +

>addsensor(80, 90, 33);
+>addsensor(50, 0, 10, 1);
+>addtimer(3);
+ +The AI section will act as the brain of your tank. Each AI line is +separated into a group of conditions functions and a group of action +functions. If all the conditions are satisfactory (true), all of the actions +are given as orders. Conditions are separated by ampersands, actions separated +by periods. Here are some examples of AI commands: +
+sensor(1) & sensor(2) & fireready() : fire();
+sensor(0,0)&sin(5): move(40, 30) . turretcw(50);
+sensor(4) & random(4,5) : led(1).settoggle(0,1);
+ +Your tank will check its program each turn, and attempt to the best of its +abilities to carry out its orders (or die trying). Like any military mind, +your tank may receive a plethora of often conflicting orders and information. +This a SMART TANK, however. It knows that the proper thing to do with each +subsystem is to have that subsystem follow only the last order given each turn. +""" + +import traceback +import conditions +import actions +import setup + +class Statement(object): + """Represents a single program statement. If all the condition Functions + evaluate to True, the actions are all executed in order.""" + + def __init__(self, lineNum, line, conditions, actions): + self.lineNum = lineNum + self.line = line + self._conditions = conditions + self._actions = actions + + def __call__(self, tank): + success = True + for condition in self._conditions: + if not condition(tank): + success = False + break + + if success: + for action in self._actions: + action(tank) + +class Program(object): + """This parses and represents a Tank program.""" + CONDITION_SEP = '&' + ACTION_SEP = '.' + + def __init__(self, text): + """Initialize this program, parsing the given text.""" + self.errors = [] + + self._program, self._setup = self._parse(text) + + def setup(self, tank): + """Execute all the setup actions.""" + for action in self._setup: + try: + action(tank) + except Exception, msg: + self.errors.append("Bad setup action, line %d, msg: %s" % \ + (action.lineNum, msg)) + + def __call__(self, tank): + """Execute this program on the given tank.""" + for statement in self._program: + try: + statement(tank) + except Exception, msg: + traceback.print_exc() + self.errors.append('Error executing program. \n' + '(%d) - %s\n' + 'msg: %s\n' % + (statement.lineNum, statement.line, msg) ) + + def _parse(self, text): + """Parse the text of the given program.""" + program = [] + setup = [] + inSetup = True + lines = text.split(';') + lineNum = 0 + for line in lines: + lineNum = lineNum + 1 + + originalLine = line + + # Remove Comments + parts = line.split('\n') + for i in range(len(parts)): + comment = parts[i].find('#') + if comment != -1: + parts[i] = parts[i][:comment] + # Remove all whitespace + line = ''.join(parts) + line = line.replace('\r', '') + line = line.replace('\t', '') + line = line.replace(' ', '') + + if line == '': + continue + + if line.startswith('>'): + if inSetup: + if '>' in line[1:] or ':' in line: + self.errors.append('(%d) Missing semicolon: %s' % + (lineNum, line)) + continue + + try: + setupAction = self._parseSection(line[1:], 'setup')[0] + setupAction.lineNum = lineNum + setup.append(setupAction) + except Exception, msg: + self.errors.append('(%d) Error parsing setup line: %s' + '\nThe error was: %s' % + (lineNum, originalLine, msg)) + + continue + else: + self.errors.append('(%d) Setup lines aren\'t allowed ' + 'after the first command: %s' % + (lineNum, originalLine)) + else: + # We've hit the first non-blank, non-comment, non-setup + # line + inSetup = False + + semicolons = line.count(':') + if semicolons > 1: + self.errors.append('(%d) Missing semicolon: %s' % + (lineNum, line)) + continue + elif semicolons == 1: + conditions, actions = line.split(':') + else: + self.errors.append('(%d) Invalid Line, no ":" seperator: %s'% + (lineNum, line) ) + + try: + conditions = self._parseSection(conditions, 'condition') + except Exception, msg: + self.errors.append('(%d) %s - "%s"' % + (lineNum, msg, line) ) + continue + + try: + actions = self._parseSection(actions, 'action') + except Exception, msg: + self.errors.append('(%d) %s - "%s"' % + (lineNum, msg, originalLine) ) + continue + program.append(Statement(lineNum, line, conditions, actions)) + + return program, setup + + def _parseSection(self, section, sectionType): + """Parses either the action or condition section of each command. +@param section: The text of the section of the command to be parsed. +@param sectionType: The type of section to be parsed. Should be: + 'condition', 'action', or 'setup'. +@raises ValueError: Raises ValueErrors when parsing errors occur. +@returns: Returns a list of parsed section components (Function objects). + """ + + if sectionType == 'condition': + parts = section.split(self.CONDITION_SEP) + functions = conditions.conditions + if section == '': + return [] + elif sectionType == 'action': + parts = section.split(self.ACTION_SEP) + functions = actions.actions + if section == '': + raise ValueError("The action section cannot be empty.") + elif sectionType == 'setup': + parts = [section] + functions = setup.setup + else: + raise ValueError('Invalid section Type - Contact Contest Admin') + + parsed = [] + for part in parts: + + pos = part.find('(') + if pos == -1: + raise ValueError("Missing open paren in %s: %s" % + (sectionType, part) ) + funcName = part[:pos] + + if funcName not in functions: + raise ValueError("%s function %s is not accepted." % + (sectionType.capitalize(), funcName) ) + + if part[-1] != ')': + raise ValueError("Missing closing paren in %s: %s" % + (condition, sectionType) ) + + args = part[pos+1:-1] + if args != '': + args = args.split(',') + for i in range(len(args)): + args[i] = int(args[i]) + else: + args = [] + + parsed.append(functions[funcName](*args)) + + return parsed diff --git a/build/lib/tanks/Tank.py b/build/lib/tanks/Tank.py new file mode 100644 index 0000000..1c9b32e --- /dev/null +++ b/build/lib/tanks/Tank.py @@ -0,0 +1,479 @@ +import math +import random +from sets import Set as set + +import GameMath as gm +import Program + +class Tank(object): + + # How often, in turns, that we can fire. + FIRE_RATE = 20 + # How far the laser shoots from the center of the tank + FIRE_RANGE = 45.0 + # The radius of the tank, from the center of the turret. + # This is what is used for collision and hit detection. + RADIUS = 7.5 + # Max speed, in pixels + SPEED = 7.0 + # Max acceleration, as a fraction of speed. + ACCEL = 35 + # Sensor range, in pixels + SENSOR_RANGE = 90.0 + # Max turret turn rate, in radians + TURRET_TURN_RATE = math.pi/10 + + # The max number of sensors/timers/toggles + SENSOR_LIMIT = 10 + + def __init__(self, name, pos, color, boardSize, angle=None, tAngle=None, + testMode=True): + """Create a new tank. +@param name: The name name of the tank. Stored in self.name. +@param pos: The starting position of the tank (x,y) +@param color: The color of the tank. +@param boardSize: The size of the board. (maxX, maxY) +@param angle: The starting angle of the tank, defaults to random. +@param tAngle: The starting turretAngle of the tank, defaults to random. +@param testMode: When True, extra debugging information is displayed. Namely, + arcs for each sensor are drawn, which turn white when + activated. + """ + + # Keep track of what turn number it is for this tank. + self._turn = 0 + + self.name = name + self._testMode = testMode + + assert len(pos) == 2 and pos[0] > 0 and pos[1] > 0, \ + 'Bad starting position: %s' % str(pos) + self.pos = pos + + # The last speed of each tread (left, right) + self._lastSpeed = 0.0, 0.0 + # The next speed that the tank should try to attain. + self._nextMove = 0,0 + + # When set, the led is drawn on the tank. + self.led = False + + assert len(boardSize) == 2 and boardSize[0] > 0 and boardSize[1] > 0 + # The limits of the playfield (maxX, maxY) + self._limits = boardSize + + # The current angle of the tank. + if angle is None: + self._angle = random.random()*2*math.pi + else: + self._angle = angle + + # The current angle of the turret + if tAngle is None: + self._tAngle = random.random()*2*math.pi + else: + self._tAngle = tAngle + + self.color = color + + # You can't fire until fireReady is 0. + self._fireReady = self.FIRE_RATE + # Means the tank will fire at it's next opportunity. + self._fireNow = False + # True when the tank has fired this turn (for drawing purposes) + self._fired = False + + # What the desired turret angle should be (from the front of the tank). + # None means the turret should stay stationary. + self._tGoal = None + + # Holds the properties of each sensor + self._sensors = [] + # Holds the state of each sensor + self._sensorState = [] + + # The tanks toggle memory + self.toggles = [] + + # The tanks timers + self._timers = [] + + # Is this tank dead? + self.isDead = False + # The frame of the death animation. + self._deadFrame = 10 + # Death reason + self.deathReason = 'survived' + + def __repr__(self): + return '' % (self.name, self.pos[0], self.pos[1]) + + def get_turn(self): + return self._turn + turn = property(get_turn) + + def fire(self, near): + """Shoots, if ordered to and able. Returns the set of tanks + destroyed.""" + + killed = set() + if self._fireReady > 0: + # Ignore the shoot order + self._fireNow = False + + if self._fireNow: + self._fireNow = False + self._fireReady = self.FIRE_RATE + self._fired = True + + + firePoint = gm.polar2cart(self.FIRE_RANGE, + self._angle + self._tAngle) + for tank in near: + enemyPos = gm.minShift(self.pos, tank.pos, self._limits) + if gm.segmentCircleCollision(((0,0), firePoint), enemyPos, + self.RADIUS): + killed.add(tank) + else: + self._fired = False + + return killed + + def addSensor(self, range, angle, width, attachedTurret=False): + """Add a sensor to this tank. +@param angle: The angle, from the tanks front and going clockwise, + of the center of the sensor. (radians) +@param width: The width of the sensor (percent). +@param range: The range of the sensor (percent) +@param attachedTurret: If set, the sensor moves with the turret. + """ + assert range >=0 and range <= 1, "Invalid range value." + + if len(self._sensors) >= self.SENSOR_LIMIT: + raise ValueError("You can only have 10 sensors.") + + range = range * self.SENSOR_RANGE + + if attachedTurret: + attachedTurret = True + else: + attachedTurret = False + + self._sensors.append((range, angle, width, attachedTurret)) + self._sensorState.append(False) + + def getSensorState(self, key): + return self._sensorState[key] + + def setMove(self, left, right): + """Parse the speed values given, and set them for the next move.""" + + self._nextMove = left, right + + def setTurretAngle(self, angle=None): + """Set the desired angle of the turret. No angle means the turret + should remain stationary.""" + + if angle is None: + self._tGoal = None + else: + self._tGoal = gm.reduceAngle(angle) + + def setFire(self): + """Set the tank to fire, next turn.""" + self._fireNow = True + + def fireReady(self): + """Returns True if the tank can fire now.""" + return self._fireReady == 0 + + def addTimer(self, period): + """Add a timer with timeout period 'period'.""" + + if len(self._timers) >= self.SENSOR_LIMIT: + raise ValueError('You can only have 10 timers') + + self._timers.append(None) + self._timerPeriods.append(period) + + def resetTimer(self, key): + """Reset, and start the given timer, but only if it is inactive. + If it is active, raise a ValueError.""" + if self._timer[key] is None: + self._timer[key] = self._timerPeriods[key] + else: + raise ValueError("You can't reset an active timer (#%d)" % key) + + def clearTimer(self, key): + """Clear the timer.""" + self._timer[key] = None + + def checkTimer(self, key): + """Returns True if the timer has expired.""" + return self._timer[key] == 0 + + def _manageTimers(self): + """Decrement each active timer.""" + for i in range(len(self._timers)): + if self._timers[i] is not None and \ + self._timers[i] > 0: + self._timers[i] = self._timers[i] - 1 + + def program(self, text): + """Set the program for this tank.""" + + self._program = Program.Program(text) + self._program.setup(self) + + def execute(self): + """Execute this tanks program.""" + + # Decrement the active timers + self._manageTimers() + self.led = False + + self._program(self) + + self._move(self._nextMove[0], self._nextMove[1]) + self._moveTurret() + if self._fireReady > 0: + self._fireReady = self._fireReady - 1 + + self._turn = self._turn + 1 + + def sense(self, near): + """Detect collisions and trigger sensors. Returns the set of + tanks collided with, plus this one. We do both these steps at once + mainly because all the data is available.""" + + near = list(near) + polar = [] + for tank in near: + polar.append(gm.relativePolar(self.pos, tank.pos, self._limits)) + + for sensorId in range(len(self._sensors)): + # Reset the sensor + self._sensorState[sensorId] = False + + dist, sensorAngle, width, tSens = self._sensors[sensorId] + + # Adjust the sensor angles according to the tanks angles. + sensorAngle = sensorAngle + self._angle + # If the angle is tied to the turret, add that too. + if tSens: + sensorAngle = sensorAngle + self._tAngle + + while sensorAngle >= 2*math.pi: + sensorAngle = sensorAngle - 2*math.pi + + for i in range(len(near)): + r, theta = polar[i] + # Find the difference between the object angle and the sensor. + # The max this can be is pi, so adjust for that. + dAngle = gm.angleDiff(theta, sensorAngle) + + rCoord = gm.polar2cart(dist, sensorAngle - width/2) + lCoord = gm.polar2cart(dist, sensorAngle + width/2) + rightLine = ((0,0), rCoord) + leftLine = ((0,0), lCoord) + tankRelPos = gm.minShift(self.pos, near[i].pos, self._limits) + if r < (dist + self.RADIUS): + if abs(dAngle) <= (width/2) or \ + gm.segmentCircleCollision(rightLine, tankRelPos, + self.RADIUS) or \ + gm.segmentCircleCollision(leftLine, tankRelPos, + self.RADIUS): + + self._sensorState[sensorId] = True + break + + # Check for collisions here, since we already have all the data. + collided = set() + for i in range(len(near)): + r, theta = polar[i] + if r < (self.RADIUS + near[i].RADIUS): + collided.add(near[i]) + + # Add this tank (a collision kills both, after all + if collided: + collided.add(self) + + return collided + + def die(self, reason): + self.isDead = True + self.deathReason = reason + + def _moveTurret(self): + if self._tGoal is None or self._tAngle == self._tGoal: + return + + diff = gm.angleDiff(self._tGoal, self._tAngle) + + if abs(diff) < self.TURRET_TURN_RATE: + self._tAngle = self._tGoal + elif diff > 0: + self._tAngle = gm.reduceAngle(self._tAngle - self.TURRET_TURN_RATE) + else: + self._tAngle = gm.reduceAngle(self._tAngle + self.TURRET_TURN_RATE) + + def _move(self, lSpeed, rSpeed): + + assert abs(lSpeed) <= 100, "Bad speed value: %s" % lSpeed + assert abs(rSpeed) <= 100, "Bad speed value: %s" % rSpeed + + # Handle acceleration + if self._lastSpeed[0] < lSpeed and \ + self._lastSpeed[0] + self.ACCEL < lSpeed: + lSpeed = self._lastSpeed[0] + self.ACCEL + elif self._lastSpeed[0] > lSpeed and \ + self._lastSpeed[0] - self.ACCEL > lSpeed: + lSpeed = self._lastSpeed[0] - self.ACCEL + + if self._lastSpeed[1] < rSpeed and \ + self._lastSpeed[1] + self.ACCEL < rSpeed: + rSpeed = self._lastSpeed[1] + self.ACCEL + elif self._lastSpeed[1] > rSpeed and \ + self._lastSpeed[1] - self.ACCEL > rSpeed: + rSpeed = self._lastSpeed[1] - self.ACCEL + + self._lastSpeed = lSpeed, rSpeed + + # The simple case + if lSpeed == rSpeed: + fSpeed = self.SPEED*lSpeed/100 + x = fSpeed*math.cos(self._angle) + y = fSpeed*math.sin(self._angle) + # Adjust our position + self._reposition((x,y), 0) + return + + # The works as follows: + # The tank drives around in a circle of radius r, which is some + # offset on a line perpendicular to the tank. The distance it travels + # around the circle varies with the speed of each tread, and is + # such that each side of the tank moves an equal angle around the + # circle. + L = self.SPEED * lSpeed/100.0 + R = self.SPEED * rSpeed/100.0 + friction = .75 * abs(L-R)/(2.0*self.SPEED) + L = L * (1 - friction) + R = R * (1 - friction) + + # Si is the speed of the tread on the inside of the turn, + # So is the speed on the outside of the turn. + # dir is to note the direction of rotation. + if abs(L) > abs(R): + Si = R; So = L + dir = 1 + else: + Si = L; So = R + dir = -1 + + # The width of the tank... + w = self.RADIUS * 2 + + # This is the angle that will determine the circle the tank travels + # around. +# theta = math.atan((So - Sl)/w) + # This is the distance from the outer tread to the center of the + # circle formed by it's movement. + r = w*So/(So - Si) + + # The fraction of the circle traveled is equal to the speed of + # the outer tread over the circumference of the circle. + # Ft = So/(2*pi*r) + # The angle traveled is equal to the Fraction traveled * 2 * pi + # This reduces to a simple: So/r + # We multiply it by dir to adjust for the direction of rotation + theta = So/r * dir + + # These are the offsets from the center of the circle, given that + # the tank is turned in some direction. The tank is facing + # perpendicular to the circle + # So far everything has been relative to the outer tread. At this + # point, however, we need to move relative to the center of the + # tank. Hence the adjustment in r. + x = -math.cos( self._angle + math.pi/2*dir ) * (r - w/2.0) + y = -math.sin( self._angle + math.pi/2*dir ) * (r - w/2.0) + + # Now we just rotate the tank's position around the center of the + # circle to get the change in coordinates. + mx, my = gm.rotatePoint((x,y), theta) + mx = mx - x + my = my - y + + # Finally, we shift the tank relative to the playing field, and + # rotate it by theta. + self._reposition((mx, my), theta) + + def _reposition(self, move, angleChange): + """Move the tank by x,y = move, and change it's angle by angle. + I assume the tanks move slower than the boardSize.""" + + x = self.pos[0] + move[0] + y = self.pos[1] + move[1] + self._angle = self._angle + angleChange + + if x < 0: + x = self._limits[0] + x + elif x > self._limits[0]: + x = x - self._limits[0] + + if y < 0: + y = self._limits[1] + y + elif y > self._limits[1]: + y = y - self._limits[1] + + self.pos = round(x), round(y) + + while self._angle < 0: + self._angle = self._angle + math.pi * 2 + + while self._angle > math.pi * 2: + self._angle = self._angle - math.pi * 2 + + def draw(self, f): + """Output this tank's state as JSON. + + [color, x, y, angle, turret_angle, led, fired] + + """ + + f.write(' [') + f.write(str(int(self.isDead))); + f.write(',') + f.write(repr(self.color)) + f.write(',') + f.write('%d' % self.pos[0]) + f.write(',') + f.write('%d' % self.pos[1]) + f.write(',') + f.write('%.2f' % self._angle) + f.write(',') + f.write('%.2f' % self._tAngle) + f.write(',') + f.write(str(int(self.led))) + f.write(',') + f.write('%d' % (self._fired and self.FIRE_RANGE) or 0) + if not self.isDead: + f.write(',[') + for i in range(len(self._sensors)): + dist, sensorAngle, width, tSens = self._sensors[i] + + # If the angle is tied to the turret, add that. + if tSens: + sensorAngle = sensorAngle + self._tAngle + + f.write('[') + f.write(str(int(dist))) + f.write(',') + f.write('%.2f' % (sensorAngle - width/2)); + f.write(',') + f.write('%.2f' % (sensorAngle + width/2)); + f.write(',') + f.write(str(int(self._sensorState[i]))) + f.write('],') + f.write(']') + + f.write('],\n') diff --git a/build/lib/tanks/__init__.py b/build/lib/tanks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tanks/actions.py b/build/lib/tanks/actions.py new file mode 100644 index 0000000..a03e7af --- /dev/null +++ b/build/lib/tanks/actions.py @@ -0,0 +1,126 @@ +"""Define new action Functions here. They should inherit from the +Function.Function class. To make an action usable, add it to the +actions dictionary at the end of this file.""" + +import Function + +class Move(Function.Function): + """move(left tread speed, right tread speed) + Set the speeds for the tanks right and left treads. The speeds should + be a number (percent power) between -100 and 100.""" + + def __init__(self, left, right): + self._checkRange(left, 'left tread speed', min=-100) + self._checkRange(right, 'right tread speed', min=-100) + + self._left = left + self._right = right + + def __call__(self, tank): + tank.setMove(self._left, self._right) + +class TurretCounterClockwise(Function.Function): + """turretccw([percent speed]) + Rotate the turret counter clockwise as a percentage of the max speed.""" + def __init__(self, speed=100): + self._checkRange(speed, 'turret percent speed') + self._speed = speed/100.0 + def __call__(self, tank): + tank.setTurretAngle(tank._tAngle - tank.TURRET_TURN_RATE*self._speed) + +class TurretClockwise(Function.Function): + """turretcw([percent speed]) + Rotate the turret clockwise at a rate preportional to speed.""" + + def __init__(self, speed=100): + self._checkRange(speed, 'turret percent speed') + self._speed = speed/100.0 + def __call__(self, tank): + tank.setTurretAngle(tank._tAngle + tank.TURRET_TURN_RATE*self._speed) + +class TurretSet(Function.Function): + """turretset([angle]) + Set the turret to the given angle, in degrees, relative to the front of + the tank. Angles increase counterclockwise. + The angle can be left out; in that case, this locks the turret + to it's current position.""" + + def __init__(self, angle=None): + # Convert the angle to radians + if angle is not None: + angle = self._convertAngle(angle, 'turret angle') + + self._angle = angle + + def __call__(self, tank): + tank.setTurretAngle(self._angle) + +class Fire(Function.Function): + """fire() + Attempt to fire the tanks laser cannon.""" + + def __call__(self, tank): + tank.setFire() + +class SetToggle(Function.Function): + """settoggle(key, state) +Set toggle 'key' to 'state'. +""" + def __init__(self, key, state): + self._key = key + self._state = state + def __call__(self, tank): + tank.toggles[self._key] = self._state + +class Toggle(Function.Function): + """toggle(key) +Toggle the value of toggle 'key'. +""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + try: + tank.toggles[self._key] = not tank.toggles[self._key] + except IndexError: + raise IndexError('Invalid toggle: %d' % self._key) + +class LED(Function.Function): + """led(state) +Set the tanks LED to state (true is on, false is off). +The led is a light that appears behind the tanks turret. +It remains on for a single turn.""" + def __init__(self, state=1): + self._state = state + def __call__(self, tank): + tank.led = self._state + +class StartTimer(Function.Function): + """starttimer(#) +Start (and reset) the given timer, but only if it is inactive. +""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.resetTimer(key) + +class ClearTimer(Function.Function): + """cleartimer(#) +Clear the given timer such that it is no longer active (inactive timers +are always False).""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.clearTimer(self._key) + +### When adding names to this dict, make sure they are lower case and alpha +### numeric. +actions = {'move': Move, + 'turretccw': TurretCounterClockwise, + 'turretcw': TurretClockwise, + 'turretset': TurretSet, + 'fire': Fire, + 'settoggle': SetToggle, + 'toggle': Toggle, + 'led': LED, + 'starttimer': StartTimer, + 'cleartimer': ClearTimer} diff --git a/build/lib/tanks/conditions.py b/build/lib/tanks/conditions.py new file mode 100644 index 0000000..1401d54 --- /dev/null +++ b/build/lib/tanks/conditions.py @@ -0,0 +1,126 @@ +"""Define new condition functions here. Add it to the conditions dictionary +at the end to make it usable by Program.Program. These should inherit from +Function.Function.""" + +import Function +import math +import random + +class Sense(Function.Function): + """sense(#, [invert]) + Takes a Sensor number as an argument. + Returns True if the given sensor is currently activated, False otherwise. + If the option argument invert is set to true then logic is inverted, + and then sensor returns True when it is NOT activated, and False when + it is. Invert is false by default. + """ + + def __init__(self, sensor, invert=0): + self._sensor = sensor + self._invert = invert + + def __call__(self, tank): + state = tank.getSensorState(self._sensor) + if self._invert: + return not state + else: + return state + +class Toggle(Function.Function): + """toggle(#) +Returns True if the given toggle is set, False otherwise. """ + def __init__(self, toggle): + self._toggle = toggle + def __call__(self, tank): + return tank.toggles[toggle] + +class TimerCheck(Function.Function): + """timer(#, [invert]) +Checks the state of timer # 'key'. Returns True if time has run out. +If invert is given (and true), then True is returned if the timer has +yet to expire. +""" + def __init__(self, key, invert=0): + self._key = key + self._invert = invert + def __call__(self, tank): + state = tank.checkTimer(self._key) + if invert: + return not state + else: + return state + +class Random(Function.Function): + """random(n,m) + Takes two arguments, n and m. Generates a random number between 1 + and m (inclusive) each time it's checked. If the random number is less + than or equal + to n, then the condition returns True. Returns False otherwise.""" + + def __init__(self, n, m): + self._n = n + self._m = m + + def __call__(self, tank): + if random.randint(1,self._m) <= self._n: + return True + else: + return False + +class Sin(Function.Function): + """sin(T) + A sin wave of period T (in turns). Returns True when the wave is positive. + A wave with period 1 or 2 is always False (it's 0 each turn), only + at periods of 3 or more does this become useful.""" + + def __init__(self, T): + self._T = T + + def __call__(self, tank): + turn = tank.turn + factor = math.pi/self._T + if math.sin(turn * factor) > 0: + return True + else: + return False + +class Cos(Function.Function): + """cos(T) + A cos wave with period T (in turns). Returns True when the wave is + positive. A wave of period 1 is always True. Period 2 is True every + other turn, etc.""" + + def __init__(self, T): + self._T = T + + def __call__(self, tank): + + turn = tank.turn + factor = math.pi/self._T + if math.cos(turn * factor) > 0: + return True + else: + return False + +class FireReady(Function.Function): + """fireready() + True when the tank can fire.""" + def __call__(self, tank): + return tank.fireReady() + +class FireNotReady(Function.Function): + """firenotready() + True when the tank can not fire.""" + def __call__(self, tank): + return not tank.fireReady() + +### When adding names to this dict, make sure they are lower case and alpha +### numeric. +conditions = {'sense': Sense, + 'random': Random, + 'toggle': Toggle, + 'sin': Sin, + 'cos': Cos, + 'fireready': FireReady, + 'firenotready': FireNotReady, + 'timer': TimerCheck } diff --git a/build/lib/tanks/docs.py b/build/lib/tanks/docs.py new file mode 100644 index 0000000..0318250 --- /dev/null +++ b/build/lib/tanks/docs.py @@ -0,0 +1,26 @@ +import xml.sax.saxutils + +def mkDocTable(objects): + objects.sort(lambda o1, o2: cmp(o1.__doc__, o2.__doc__)) + + for object in objects: + if object.__doc__ is None: + print '
%s
Bad object
' % \ + xml.sax.saxutils.escape(str(object)) + continue + text = object.__doc__ + lines = text.split('\n') + head = lines[0].strip() + head = xml.sax.saxutils.escape(head) + + body = [] + for line in lines[1:]: + line = line.strip() #xml.sax.saxutils.escape( line.strip() ) + line = line.replace('.', '.
') + body.append(line) + + body = '\n'.join(body) + print '
%s
%s
' % (head, body) + #print '%sIntentionally blank%s' % (head, body) + + diff --git a/build/lib/tanks/setup.py b/build/lib/tanks/setup.py new file mode 100644 index 0000000..81a402c --- /dev/null +++ b/build/lib/tanks/setup.py @@ -0,0 +1,72 @@ +"""Each of these classes provides a function for configuring a tank. +They should inherit from Function.Function. +To make one available to the tank programmer, add it to the dictionary at +the end of this file.""" + +import Function + +class AddSensor(Function.Function): + """addsensor(range, angle, width, [turretAttached]) +Add a new sensor to the tank. Sensors are an arc (pie slice) centered on +the tank that detect other tanks within their sweep. +A sensor is 'on' if another tank is within this arc. +Sensors are numbered, starting at 0, in the order they are added. +

+range - The range of the sensor, as a percent of the tanks max range. +angle - The angle of the center of the sensor, in degrees. +width - The width of the sensor, in percent (100 is a full circle). +turretAttached - Normally, the angle is relative to the front of the +tank. When this is set, the angle is relative to the current turret +direction. +

+Sensors are drawn for each tank, but not in the way you might expect. +Instead of drawing a pie slice (the actual shap of the sensor), an arc with +the end points connected by a line is drawn. Sensors with 0 width don't show +up, but still work. +""" + + def __init__(self, range, angle, width, turretAttached=False): + + self._checkRange(range, 'sensor range') + + self._range = range / 100.0 + self._width = self._convertAngle(width, 'sensor width') + self._angle = self._convertAngle(angle, 'sensor angle') + self._turretAttached = turretAttached + + def __call__(self, tank): + tank.addSensor(self._range, self._angle, self._width, + self._turretAttached) + +class AddToggle(Function.Function): + """addtoggle([state]) +Add a toggle to the tank. The state of the toggle defaults to 0 (False). +These essentially act as a single bit of memory. +Use the toggle() condition to check its state and the settoggle, cleartoggle, +and toggle actions to change the state. Toggles are named numerically, +starting at 0. +""" + def __init__(self, state=0): + self._state = state + + def __call__(self, tank): + if len(tank.toggles) >= tank.SENSOR_LIMIT: + raise ValueError('You can not have more than 10 toggles.') + + tank.toggles.append(self._state) + +class AddTimer(Function.Function): + """addtimer(timeout) +Add a new timer (they're numbered in the order added, starting from 0), +with the given timeout. The timeout is in number of turns. The timer +is created in inactive mode. You'll need to do a starttimer() action +to reset and start the timer. When the timer expires, the timer() +condition will begin to return True.""" + def __init__(self, timeout): + self._timeout = timeout + def __call__(self, tank): + tank.addTimer(timeout) + +setup = {'addsensor': AddSensor, + 'addtoggle': AddToggle, + 'addtimer': AddTimer} diff --git a/ctf/__init__.py b/ctf/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/ctf/flagger.py b/ctf/flagger.py new file mode 100755 index 0000000..04a9fa8 --- /dev/null +++ b/ctf/flagger.py @@ -0,0 +1,33 @@ +#! /usr/bin/python + +import asynchat +import asyncore +import socket + +class Flagger(asynchat.async_chat): + """Use to connect to flagd and submit the current flag holder.""" + + def __init__(self, addr, auth): + asynchat.async_chat.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((addr, 1)) + self.push(auth + '\n') + self.flag = None + + def handle_read(self): + # We don't care. + msg = self.recv(4096) + + 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 = '' + self.push(eteam + '\n') + self.flag = team diff --git a/ctf/html.py b/ctf/html.py new file mode 100755 index 0000000..101c4d7 --- /dev/null +++ b/ctf/html.py @@ -0,0 +1,35 @@ +#! /usr/bin/python + +import os +import string +import sys +from codecs import open + +from paths import * + +template_fn = os.path.join(LIB, 'template.html') +template = string.Template(open(template_fn, encoding='utf-8').read()) + +base = BASE_URL +css = base + 'ctf.css' + +def substitute(title, body, base=base, hdr='', body_class='', onload='', links=''): + return template.substitute(title=title, + hdr=hdr, + body_class=body_class, + base=base, + links=links, + onload=onload, + body=body) + +def serve(title, body, **kwargs): + out = substitute(title, body, **kwargs) + print 'Content-type: text/html' + print 'Content-length: %d' % len(out) + print + sys.stdout.write(out) + sys.stdout.flush() + +def write(filename, title, body, **kwargs): + f = open(filename, 'w', encoding='utf-8') + f.write(substitute(title, body, **kwargs)) diff --git a/ctf/paths.py b/ctf/paths.py new file mode 100644 index 0000000..336c75c --- /dev/null +++ b/ctf/paths.py @@ -0,0 +1,6 @@ +VAR = "/opt/ctf/var" +WWW = "/opt/ctf/www" +LIB = "/opt/ctf/lib" +BIN = "/opt/ctf/bin" +SBIN = "/opt/ctf/sbin" +BASE_URL = "/ctf/" diff --git a/ctf/pointscli.py b/ctf/pointscli.py new file mode 100755 index 0000000..5671ebc --- /dev/null +++ b/ctf/pointscli.py @@ -0,0 +1,40 @@ +#! /usr/bin/python + +from urllib import quote +import teams +import time +import os +import paths + +pointsdir = os.path.join(paths.VAR, 'points') + +def award(cat, team, points): + if not team: + team = teams.house + now = time.strftime('%Y-%m-%dT%H:%M:%S') + pid = os.getpid() + qcat = quote(cat, '') + qteam = quote(team, '') + basename = '%s.%d.%s.%s' % (now, pid, qcat, qteam) + # FAT can't handle : + basename = basename.replace(':', '.') + tmpfn = os.path.join(pointsdir, 'tmp', basename) + curfn = os.path.join(pointsdir, 'cur', basename) + f = open(tmpfn, 'w') + f.write('%s\t%s\t%s\t%d\n' % (now, cat, team, points)) + f.close() + os.rename(tmpfn, curfn) + +def main(): + import optparse + + p = optparse.OptionParser('%prog CATEGORY TEAM POINTS') + opts, args = p.parse_args() + if len(args) != 3: + p.error('Wrong number of arguments') + cat, team, points = args + points = int(points) + award(cat, team, points) + +if __name__ == '__main__': + main() diff --git a/ctf/teams.py b/ctf/teams.py new file mode 100755 index 0000000..3e9407c --- /dev/null +++ b/ctf/teams.py @@ -0,0 +1,72 @@ +#! /usr/bin/python + +import fcntl +import time +import os +from urllib import quote, unquote +import paths + +house = 'dirtbags' +passwdfn = os.path.join(paths.VAR, 'passwd') +team_colors = ['F0888A', '88BDF0', '00782B', '999900', 'EF9C00', + 'F4B5B7', 'E2EFFB', '89CA9D', 'FAF519', 'FFE7BB', + 'BA88F0', '8DCFF4', 'BEDFC4', 'FFFAB2', 'D7D7D7', + 'C5B9D7', '006189', '8DCB41', 'FFCC00', '898989'] + +teams = {} +built = 0 +def build_teams(): + global teams, built + if not os.path.exists(passwdfn): + return + if os.path.getmtime(passwdfn) <= built: + return + + teams = {} + try: + f = open(passwdfn) + for line in f: + line = line.strip() + if not line: + continue + team, passwd, color = map(unquote, line.strip().split('\t')) + teams[team] = (passwd, color) + except IOError: + pass + built = time.time() + +def validate(team): + build_teams() + +def chkpasswd(team, passwd): + validate(team) + if teams.get(team, [None, None])[0] == passwd: + return True + else: + return False + +def exists(team): + validate(team) + if team == house: + return True + return team in teams + +def add(team, passwd): + build_teams() + color = team_colors[len(teams)%len(team_colors)] + + assert team not in teams, "Team already exists." + + f = open(passwdfn, 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.seek(0, 2) + f.write('%s\t%s\t%s\n' % (quote(team, ''), + quote(passwd, ''), + quote(color, ''))) + +def color(team): + validate(team) + t = teams.get(team) + if not t: + return '888888' + return t[1] diff --git a/lib/tanks/easy/berzerker b/lib/tanks/easy/berzerker new file mode 100644 index 0000000..e0351d0 --- /dev/null +++ b/lib/tanks/easy/berzerker @@ -0,0 +1,5 @@ +random(1,10): move(50, 100); +random(1,10): move(100, 50); +random(1,10): turretcw(); +random(1,10): turretccw(); +random(1,30): fire(); \ No newline at end of file diff --git a/lib/tanks/easy/rabbitwithgun b/lib/tanks/easy/rabbitwithgun new file mode 100644 index 0000000..b820488 --- /dev/null +++ b/lib/tanks/easy/rabbitwithgun @@ -0,0 +1,8 @@ +>addsensor(50, 0, 0, 1); +>addsensor(70, 0, 50); # 1-Anti-collision sensor + + : move(100, 100) . turretset(180); +random(1, 8): move(70, 100); +random(1, 8): move(100, 70); +sense(0) : fire(); +sense(1) : move(-0, 100); \ No newline at end of file diff --git a/lib/tanks/hard/chashtank b/lib/tanks/hard/chashtank new file mode 100755 index 0000000..05d7600 --- /dev/null +++ b/lib/tanks/hard/chashtank @@ -0,0 +1,52 @@ + +>addsensor(50, 0, 10, 1); +>addsensor(35, 0, 90, 0); +>addsensor(100, 30, 59, 0); +>addsensor(100, 330, 59, 0); +>addsensor(70, 180, 180); +>addsensor(100, 90, 59, 0); +>addsensor(100, 270, 59, 0); +>addsensor(100, 0, 5, 1); + +>addsensor(55, 50, 89, 0); +>addsensor(55, 310, 89, 0); + +# move back and forth + : move(90,90).turretset(0); +random(2,6) : move(95,75).turretset(0); +#random(1,6) : move(75,95).turretset(0); + +# rear sensor +sense(4) : move(90, 90); + +# far right front sensor +sense(5) : move(100,-100); + +# far left front sensor +sense(6) : move(-100,100); + +# right front sensor +sense(2) : move(80,-80); + +# left front sensor +sense(3) : move(-80,80); + +# immediate front sensor in firing range +sense(0) & firenotready() : move(-50, -50); + : turretset(0); + +# near far right front sensor +sense(8) : move(60,-60); +sense(9) : move(-60, 60); + +fireready() : led(); + +# front far sensor +sense(7) & fireready() : move(100,100); + +# collison sensor +sense(1) : move(-100, -100); + : turretset(0); + +sense(0) & fireready() : fire(); + diff --git a/lib/tanks/hard/crashmaster b/lib/tanks/hard/crashmaster new file mode 100644 index 0000000..ca94b98 --- /dev/null +++ b/lib/tanks/hard/crashmaster @@ -0,0 +1,57 @@ +# 3 +# 000 33 +# 2 +# 2 +# 2 +# 11111 4 +# 4 +# 4 +# @@/ +# @@@ +# @@@ +# +# +# +# + + +>addsensor(50, 0, 05, 1); # 0 Fire Sensor +>addsensor(30, 0, 50); # 1 Anti-collision sensor +>addsensor(50, 0, 10); # 2 Anti-collision sensor +>addsensor(100, 315, 100, 1); # 3 Turret ccw +>addsensor(100, 45, 100, 1); # 4 Turret cw +>addsensor(60, 180, 180, 0); # 5 Ass + +## +## Add "ears" so the tank is easy to pick out. +## +>addsensor(20, 90, 30, 0); +>addsensor(20, 270, 30, 0); + +# Can't fire + : led(0) . move(80, 80) . turretset(0); +random(1, 3): led(0) . move(60, 80) . turretset(0); +random(2, 3): led(0) . move(80, 60) . turretset(0); + +sense(0) : led(0) . move(10, 20) . turretset(0); +sense(1) : led(0) . move(10, 10) . turretset(0); +sense(2) : led(0) . move(10, 20) . turretset(0); +sense(3) : led(0) . move(70, 50) . turretset(0); +sense(4) : led(0) . move(50, 70) . turretset(0); +sense(3) & sense(4): led(0) . move(-100, 20) . turretset(0); +sense(5) : led(0) . move(100, 50) . turretset(0); + + +# Can fire +fireready() : led(1) . move(70, 70) . turretset(0); +fireready() & random(2, 40): led(1) . move(40, 70) . turretset(0); +fireready() & random(1, 40): led(1) . move(70, 40) . turretset(0); + +fireready() & sense(3) : led(1) . move(0, 60) . turretccw(50); +fireready() & sense(4) : led(1) . move(60, 0) . turretcw(50); +fireready() & sense(3) & sense(4): led(1) . move(100, 100) . turretset(); +fireready() & sense(1) : led(1) . turretset(0) . move(10, 10); +fireready() & sense(2) : led(1) . turretset(0) . move(10, 10); +fireready() & sense(0) : led(1) . turretset() . fire(); + +fireready() & sense(5) : led(1) . move(100, 40); \ No newline at end of file diff --git a/lib/tanks/hard/foobar b/lib/tanks/hard/foobar new file mode 100644 index 0000000..bcdf807 --- /dev/null +++ b/lib/tanks/hard/foobar @@ -0,0 +1,22 @@ +>addsensor(55, 0, 5, 1); +>addsensor(40, 0, 30); +>addsensor(80, 30, 59, 0); +>addsensor(80, 330, 59, 0); +>addsensor(70, 180, 180); +>addsensor(80, 90, 59, 0); +>addsensor(80, 270, 59, 0); + +# : move(70,80); +# random(3,6) : move(80,70); + : move(65,85); +random(2,6) : move(90,65); +sense(2) : move(80,10).turretcw(100); +sense(3) : move(10,80).turretccw(100); +sense(4) : move(90, 90); +sense(5) : move(90,10).turretcw(100); +sense(6) : move(10,90).turretccw(100); +sense(0) & fireready() : turretset().move(90,90).fire(); +sense(1) : move(-100, -100); + : turretset(0); +fireready() : led(); + diff --git a/lib/tanks/hard/pflarr b/lib/tanks/hard/pflarr new file mode 100644 index 0000000..b4df468 --- /dev/null +++ b/lib/tanks/hard/pflarr @@ -0,0 +1,31 @@ +>addsensor(50, 0, 45, 1); # 0-Fire Sensor +>addsensor(30, 0, 180); # 1-Anti-collision sensor +>addsensor(100, 40, 60, 1); # 2 turret clockwise +>addsensor(100, 320, 60, 1); # 3 turret ccw +>addsensor(80, 180, 160); # 4 Coward +>addsensor(100, 0, 0, 1); # 5-Fire Sensor2 +>addsensor(100, 0, 0); # 6-Chase Sensor +>addsensor(75, 75, 30); # 7-quick turn right +>addsensor(75, 285, 30); # 8-quick turn left + +# Commands + : move(70, 75). + turretset(0); +random(1, 10): move(75, 75). + turretset(0); +sense(2) : turretcw(50). + move(85, 70); +sense(2) & sense(0): turretcw(25). + move(85, 70); +sense(3) : turretccw(50). + move(70, 85); +sense(3) & sense(0) : turretccw(25). + move(70, 85); +sense(5) & sense(7) : move(70, 30); +sense(5) & sense(8) : move(30, 70); +#sense(5) : turretset(); +sense(0) & sense(5) : fire(); +sense(6) & sense(5) & fireready(): move(100,100); +sense(4) : move(100,100); +sense(1) : move(-50, 25); +fireready() : led(); diff --git a/lib/tanks/medium/simpleton b/lib/tanks/medium/simpleton new file mode 100644 index 0000000..64fc607 --- /dev/null +++ b/lib/tanks/medium/simpleton @@ -0,0 +1,8 @@ +>addsensor(50, 0, 5, 1); # 0-Fire Sensor +>addsensor(30, 0, 50); # 1-Anti-collision sensor + +# Commands + : move(90, 100). + turretset(0); +sense(0) : fire(); +sense(1) : move(-100, 100) diff --git a/lib/tanks/medium/sittingduckwithteeth b/lib/tanks/medium/sittingduckwithteeth new file mode 100644 index 0000000..4ea551f --- /dev/null +++ b/lib/tanks/medium/sittingduckwithteeth @@ -0,0 +1,9 @@ +>addsensor(50, 0, 10, 1); # 0-Fire Sensor +>addsensor(100, 90, 150, 1); +>addsensor(100, 270, 150, 1); + +: turretcw(75); +sense(0): fire(); +sense(1): turretcw(); +sense(2): turretccw(); + diff --git a/lib/tanks/medium/sweeper b/lib/tanks/medium/sweeper new file mode 100644 index 0000000..e02e950 --- /dev/null +++ b/lib/tanks/medium/sweeper @@ -0,0 +1,18 @@ +# Just sit there and sweep the field until it finds something to shoot. +# Uses a long-range sensor on the left and right to hone in. + +>addsensor(50, 0, 5, 1); # 0 +>addsensor(100, 90, 150, 1); # 1 +>addsensor(100, 270, 150, 1); # 2 +>addsensor(100, 0, 359, 0); # 3 + +# Default movement if nothing is detected + : move(70, 70) . turretccw(); +random(2, 3): move(40, 70) . turretccw(); +random(1, 3): move(70, 40) . turretccw(); + +# We found something!! +sense(3): move(0, 0); +sense(1): turretcw(); +sense(2): turretccw(); +sense(0): fire(); diff --git a/mkpuzzles.py b/mkpuzzles.py new file mode 100755 index 0000000..569097f --- /dev/null +++ b/mkpuzzles.py @@ -0,0 +1,140 @@ +#! /usr/bin/python + +import os +import shutil +import optparse +import string +import markdown +from codecs import open + +p = optparse.OptionParser() +p.add_option('-t', '--template', dest='template', default='template.html', + help='Location of HTML template') +p.add_option('-b', '--base', dest='base', default='', + help='Base URL for contest') +p.add_option('-p', '--puzzles', dest='puzzles', default='puzzles', + help='Directory containing puzzles') +p.add_option('-w', '--htmldir', dest='htmldir', default='puzzler', + help='Directory to write HTML puzzle tree') +p.add_option('-k', '--keyfile', dest='keyfile', default='puzzler.keys', + help='Where to write keys') + +opts, args = p.parse_args() + +keys = [] + +tmpl_f = open(opts.template, encoding='utf-8') +template = string.Template(tmpl_f.read()) +tmpl_f.close() + +js = ''' + +''' + +for cat in os.listdir(opts.puzzles): + dirname = os.path.join(opts.puzzles, cat) + for points in os.listdir(dirname): + pointsdir = os.path.join(dirname, points) + if not os.path.isdir(pointsdir): + continue + + outdir = os.path.join(opts.htmldir, cat, points) + try: + os.makedirs(outdir) + except OSError: + pass + + readme = '' + files = [] + for fn in os.listdir(pointsdir): + path = os.path.join(pointsdir, fn) + if fn == 'key': + for key in open(path, encoding='utf-8'): + key = key.rstrip() + keys.append((cat, points, key)) + elif fn == 'hint': + pass + elif fn == 'index.html': + readme = open(path, encoding='utf-8').read() + elif fn == 'index.mdwn': + readme = open(path, encoding='utf-8').read() + readme = markdown.markdown(readme) + elif fn.endswith('~'): + pass + else: + files.append((fn, path)) + + title = '%s for %s points' % (cat, points) + + body = [] + if readme: + body.append('

%s
\n' % readme) + if files: + body.append('
    \n') + for fn, path in files: + if os.path.isdir(path): + shutil.rmtree(os.path.join(outdir, fn), ignore_errors=True) + shutil.copytree(path, os.path.join(outdir, fn)) + else: + shutil.copy(path, outdir) + + if not fn.startswith(','): + body.append('
  • %s
  • \n' % (fn, fn)) + body.append('
\n') + body.append(''' +
+
+ Your answer: + + + Team:
+ Password:
+ Key:
+ +
+
+''' % {'base': opts.base, + 'cat': cat, + 'points': points}) + + page = template.substitute(hdr=js, + title=title, + base=opts.base, + links='', + body_class='', + onload = "getTeamInfo()", + body=''.join(body)) + + f = open(os.path.join(outdir, 'index.html'), 'w', encoding='utf-8') + f.write(page) + +f = open(opts.keyfile, 'w', encoding='utf-8') +for key in keys: + f.write('%s\t%s\t%s\n' % key) + diff --git a/puzzler.keys b/puzzler.keys new file mode 100644 index 0000000..7c492e4 --- /dev/null +++ b/puzzler.keys @@ -0,0 +1,112 @@ +bletchley 50 extra special text +bletchley 1000 It is a lovely day outside +bletchley 150 jackalope wheeze +bletchley 350 PC LOAD LETTER +bletchley 300 jako561962 +bletchley 200 unequivocal +bletchley 900 PEANUT BUTTER JELLY TIME +bletchley 100 antediluvian +bletchley 500 xez.3nt +bletchley 250 DB1663<3 +net-re 4 PINHEAD CATASTROPHE +net-re 2 great job +net-re 25000 galloping gallimimus +net-re 1000 a difficult key! +net-re 7 60.0.13.65 +net-re 4000 gaucho moped fleet +net-re 800 10.72.148.66 +net-re 5000 miniature commodore exercise +net-re 300 pumpkins +net-re 5 fishsticks +net-re 30 RSTNFGEAID +net-re 3000 galactic octopus +net-re 200 particulate +net-re 6 whatever.example.net +net-re 2000 obtuse +net-re 3 2,4,6,8,A,B,C,D,F +net-re 400 lettuce +net-re 100 chumbucket +net-re 8 bacon +net-re 1200 hotshot tomato +net-re 20 squirrel +net-re 1 163 +net-re 10 69.35 +net-re 700 fixate rasterize +net-re 250 alice_test@hotmail.com +survey 1000000 quux blorb frotz +sequence 50 42 +sequence 2 111 1000 +sequence 35 13 21 +sequence 300 ─ +sequence 25 36 +sequence 600 61 64 9b +sequence 200 E G G +sequence 16 a +sequence 400 0101 +sequence 100 45 +sequence 8 100 +sequence 19 17 +sequence 1 6 +sequence 700 01 00 00 ca 0a +sequence 450 05 +hispaniola 50 LANL GUYS +hispaniola 5 3acd767f2717b84076cdcd18e882f01d +hispaniola 125 β€½ +hispaniola 75 βš‘ β—’ β—• β˜… β™₯ β—’ β™₯ βš‘ β—• β˜… β™₯ β—• β˜… β™₯ βš‘ β˜… βš‘ β—’ β™₯ β—’ β—• β—• β—’ β˜… βš‘ +hispaniola 15 -462766 +hispaniola 10 You're well on your way :) +skynet 302 SkynetSasserVersionWithPingFast +skynet 401 67678dj*&78 +skynet 102 beagle_beagle +skynet 300 pecompact +skynet 202 tftp +skynet 203 FreeConsole +skynet 200 402fcc +skynet 301 4028de +skynet 400 services.exe +skynet 100 bbeagle.exe +skynet 500 c:\windows\system32:lzx32.sys +skynet 500 +skynet 501 kdD +crypto 160 chronic failure +crypto 220 open meadows +crypto 230 quavering tendrils +crypto 130 probable cause +crypto 210 The Colour Out of Space +crypto 150 flaming mastiff +crypto 240 in the same vein +crypto 140 giant chickens +crypto 200 the squirrels crow at noon +crypto 110 the s is for sucks +crypto 120 Rat Fink +crypto 180 The key for this puzzle is this sentence +crypto 400 --------========Thanks for Pl@y|ng========-------- +crypto 100 caesar +crypto 170 terrifying silence +crypto 1 dirtbags +forensics 50 C:\WINDOWS\system32\klog.sys +forensics 200 winsecur.dll +forensics 400 avatar.txt +forensics 100 dll injection +forensics 20 klog.txt +forensics 10 lsass.exe +forensics 250 Dirka Dirka +compaq 50 4C +compaq 150 This is our world now... the world of the electron and the switch, the beauty of the baud. +compaq 350 Actually, Werner, we're all tickled to here you say that. Frankly, watchin' Donny beat Nazis to death is is the closest we ever get to goin' to the movies. +compaq 600 Now think real hard. You been bird-doggin' this township awhile now. They wouldn't mind a corpse of you. Now, you can luxuriate in a nice jail cell, but if your hand touches metal, I swear by my pretty floral bonnet, I will end you. +compaq 200 Gawain Ballard Tunisia +compaq 400 lawful forths Amanda +compaq 100 root:x:0:0:root:/root:/bin/bash +compaq 500 codger launched jet +hackme 806 That's all, folks. +hackme 200 james +hackme 614 james +webapp 50 eVkIwHzOok +webapp 40 765JBo4B54 +webapp 70 s4nNlaMScV +webapp 30 BRrHdtdADI +webapp 60 QJebByJaKX +webapp 80 dmW5f9P54e +webapp 20 uq4G4dXrpx +webapp 10 ktFfb8R1Bw diff --git a/puzzles/bletchley/100/key b/puzzles/bletchley/100/key new file mode 100644 index 0000000..5403edd --- /dev/null +++ b/puzzles/bletchley/100/key @@ -0,0 +1 @@ +antediluvian diff --git a/puzzles/bletchley/100/key.png b/puzzles/bletchley/100/key.png new file mode 100644 index 0000000..b658ad9 Binary files /dev/null and b/puzzles/bletchley/100/key.png differ diff --git a/puzzles/bletchley/1000/index.mdwn b/puzzles/bletchley/1000/index.mdwn new file mode 100644 index 0000000..7dd37e9 --- /dev/null +++ b/puzzles/bletchley/1000/index.mdwn @@ -0,0 +1,108 @@ +Safe to execute. + +Santa's helpers binary diff --git a/puzzles/bletchley/1000/key b/puzzles/bletchley/1000/key new file mode 100644 index 0000000..3be61b9 --- /dev/null +++ b/puzzles/bletchley/1000/key @@ -0,0 +1 @@ +It is a lovely day outside diff --git a/puzzles/bletchley/150/aacaaebb0cd0503e7bad97c42321a738 b/puzzles/bletchley/150/aacaaebb0cd0503e7bad97c42321a738 new file mode 100644 index 0000000..6ea1d2e Binary files /dev/null and b/puzzles/bletchley/150/aacaaebb0cd0503e7bad97c42321a738 differ diff --git a/puzzles/bletchley/150/index.mdwn b/puzzles/bletchley/150/index.mdwn new file mode 100644 index 0000000..672516c --- /dev/null +++ b/puzzles/bletchley/150/index.mdwn @@ -0,0 +1,2 @@ +Recovery, while not strictly necessary, may be tremendously helpful. + diff --git a/puzzles/bletchley/150/key b/puzzles/bletchley/150/key new file mode 100644 index 0000000..1349adc --- /dev/null +++ b/puzzles/bletchley/150/key @@ -0,0 +1 @@ +jackalope wheeze \ No newline at end of file diff --git a/puzzles/bletchley/200/index.mdwn b/puzzles/bletchley/200/index.mdwn new file mode 100644 index 0000000..3b9ced7 --- /dev/null +++ b/puzzles/bletchley/200/index.mdwn @@ -0,0 +1 @@ + tkftsuiuqvaheohrnsnuoleyriod"eic" diff --git a/puzzles/bletchley/200/key b/puzzles/bletchley/200/key new file mode 100644 index 0000000..d398ae2 --- /dev/null +++ b/puzzles/bletchley/200/key @@ -0,0 +1 @@ +unequivocal diff --git a/puzzles/bletchley/250/index.mdwn b/puzzles/bletchley/250/index.mdwn new file mode 100644 index 0000000..003362a --- /dev/null +++ b/puzzles/bletchley/250/index.mdwn @@ -0,0 +1 @@ + 27586126814341379597440261571645814840581961154587430529221052323 diff --git a/puzzles/bletchley/250/key b/puzzles/bletchley/250/key new file mode 100644 index 0000000..3933ee2 --- /dev/null +++ b/puzzles/bletchley/250/key @@ -0,0 +1 @@ +DB1663<3 diff --git a/puzzles/bletchley/300/index.mdwn b/puzzles/bletchley/300/index.mdwn new file mode 100644 index 0000000..e6bca1d --- /dev/null +++ b/puzzles/bletchley/300/index.mdwn @@ -0,0 +1,22 @@ +Kolejne modele Panzerfausta, odpowiednio: 60, 100, 150, rΓ³ΕΌniΕ‚ +kaliber gΕ‚owicy i wielkoΕ›Δ‡ Ε‚adunku miotajΔ…cego. Konstrukcja i +mechanizm nie ulegaΕ‚ istotnym zmianom, z racji wzrastania zasiΔ™gu +broni modyfikacjom ulegaΕ‚y nastawy celownikΓ³w. Jedynie we wzorze 150 +wprowadzono (a byΕ‚ to juΕΌ poczΔ…tek 1945 roku) wielokrotne uΕΌycie +wyrzutni rurowej. OsiΔ…gniΔ™to to przez umieszczenie Ε‚adunku +miotajΔ…cego w gΕ‚owicy oraz przez wzmocnienie rury. W wyniku problemu z +transportem model Γ³w nie wszedΕ‚ do walki. Model 250 (o teoretycznym +zasiΔ™gu 250 m) z racji zakoΕ„czenia wojny nie opuΕ›ciΕ‚ desek +kreΕ›larskich nigdy nie wchodzΔ…c nawet w fazΔ™ prototypowΔ…. + + (61, 4) + (47, 8) + (19, 4) + (37, 1) + (51, 3) + (67, 5) + (9, 2) + (26, 1) + (2, 2) + (26, 3) + (50, 2) diff --git a/puzzles/bletchley/300/key b/puzzles/bletchley/300/key new file mode 100644 index 0000000..ea86794 --- /dev/null +++ b/puzzles/bletchley/300/key @@ -0,0 +1 @@ +jako561962 diff --git a/puzzles/bletchley/350/936bc08007a9076673a81040024728be b/puzzles/bletchley/350/936bc08007a9076673a81040024728be new file mode 100644 index 0000000..25d4c2f Binary files /dev/null and b/puzzles/bletchley/350/936bc08007a9076673a81040024728be differ diff --git a/puzzles/bletchley/350/key b/puzzles/bletchley/350/key new file mode 100644 index 0000000..89709ea --- /dev/null +++ b/puzzles/bletchley/350/key @@ -0,0 +1 @@ +PC LOAD LETTER diff --git a/puzzles/bletchley/50/adddbafb502355634d9ef10e1848cf52 b/puzzles/bletchley/50/adddbafb502355634d9ef10e1848cf52 new file mode 100644 index 0000000..97e2900 Binary files /dev/null and b/puzzles/bletchley/50/adddbafb502355634d9ef10e1848cf52 differ diff --git a/puzzles/bletchley/50/key b/puzzles/bletchley/50/key new file mode 100644 index 0000000..0db4aae --- /dev/null +++ b/puzzles/bletchley/50/key @@ -0,0 +1 @@ +extra special text diff --git a/puzzles/bletchley/500/200601262232.ogg b/puzzles/bletchley/500/200601262232.ogg new file mode 100644 index 0000000..d00f825 Binary files /dev/null and b/puzzles/bletchley/500/200601262232.ogg differ diff --git a/puzzles/bletchley/500/cipher.txt b/puzzles/bletchley/500/cipher.txt new file mode 100644 index 0000000..cf43f95 --- /dev/null +++ b/puzzles/bletchley/500/cipher.txt @@ -0,0 +1 @@ +31 9 15 26 14 23 14 6 18 5 12 18 5 2 16 27 7 10 11 5 13 31 17 17 6 2 26 26 10 21 10 8 20 4 diff --git a/puzzles/bletchley/500/index.mdwn b/puzzles/bletchley/500/index.mdwn new file mode 100644 index 0000000..8c32ab6 --- /dev/null +++ b/puzzles/bletchley/500/index.mdwn @@ -0,0 +1 @@ +journals.uchicago diff --git a/puzzles/bletchley/500/key b/puzzles/bletchley/500/key new file mode 100644 index 0000000..b197a4a --- /dev/null +++ b/puzzles/bletchley/500/key @@ -0,0 +1 @@ +xez.3nt diff --git a/puzzles/bletchley/900/1d45b460b5844d0d769ca469f7b5bdc7 b/puzzles/bletchley/900/1d45b460b5844d0d769ca469f7b5bdc7 new file mode 100644 index 0000000..fbf0b16 Binary files /dev/null and b/puzzles/bletchley/900/1d45b460b5844d0d769ca469f7b5bdc7 differ diff --git a/puzzles/bletchley/900/key b/puzzles/bletchley/900/key new file mode 100644 index 0000000..4a1b0b3 --- /dev/null +++ b/puzzles/bletchley/900/key @@ -0,0 +1 @@ +PEANUT BUTTER JELLY TIME diff --git a/puzzles/bletchley/summary.txt b/puzzles/bletchley/summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/puzzles/compaq/100/index.mdwn b/puzzles/compaq/100/index.mdwn new file mode 100644 index 0000000..0c187a5 --- /dev/null +++ b/puzzles/compaq/100/index.mdwn @@ -0,0 +1,53 @@ +You are doing a forensics evaluation of a linux box that you know has +been compromised. You find a binary on the system and assume it was +used by the attackers to hide data on box that that was exfiltrated. +You dissamble the file and find the x86 assembly shown below (from Ida) +- this function was used for obfuscation. You also find a file +obfuscated by this tool. Using the key you find in this encoder code +what is the unobfuscated first line of the file which starts with + + 8%%>p2pzpzp8%%>pe8%%>pe(#$e(+9" + + HINT: The function was orginally defined as void convert_buf(unsigned +char * buf, int len). + + .text:08048474 ; =============== S U B R O U T I N E ======================================= + .text:08048474 + .text:08048474 ; Attributes: bp-based frame + .text:08048474 + .text:08048474 public convert_buf + .text:08048474 convert_buf proc near ; CODE XREF: main+B2p + .text:08048474 + .text:08048474 cnt = dword ptr -4 + .text:08048474 buf = dword ptr 8 + .text:08048474 len = dword ptr 0Ch + .text:08048474 + .text:08048474 push ebp + .text:08048475 mov ebp, esp + .text:08048477 sub esp, 10h + .text:0804847A mov [ebp+cnt], 0 + .text:08048481 mov [ebp+cnt], 0 + .text:08048488 jmp short loc_80484A4 + .text:0804848A ; --------------------------------------------------------------------------- + .text:0804848A + .text:0804848A loc_804848A: ; CODE XREF: convert_buf+36j + .text:0804848A mov eax, [ebp+cnt] + .text:0804848D mov edx, eax + .text:0804848F add edx, [ebp+buf] + .text:08048492 mov eax, [ebp+cnt] + .text:08048495 add eax, [ebp+buf] + .text:08048498 movzx eax, byte ptr [eax] + .text:0804849B xor eax, 4Ah + .text:0804849E mov [edx], al + .text:080484A0 add [ebp+cnt], 1 + .text:080484A4 + .text:080484A4 loc_80484A4: ; CODE XREF: convert_buf+14j + .text:080484A4 mov eax, [ebp+cnt] + .text:080484A7 cmp eax, [ebp+len] + .text:080484AA jl short loc_804848A + .text:080484AC leave + .text:080484AD retn + .text:080484AD convert_buf endp + .text:080484AD + .text:080484AE + diff --git a/puzzles/compaq/100/key b/puzzles/compaq/100/key new file mode 100644 index 0000000..53d2da6 --- /dev/null +++ b/puzzles/compaq/100/key @@ -0,0 +1 @@ +root:x:0:0:root:/root:/bin/bash \ No newline at end of file diff --git a/puzzles/compaq/150/b2f3f6b43ecadc7ae0b5f0edde694c78 b/puzzles/compaq/150/b2f3f6b43ecadc7ae0b5f0edde694c78 new file mode 100755 index 0000000..c20b65b Binary files /dev/null and b/puzzles/compaq/150/b2f3f6b43ecadc7ae0b5f0edde694c78 differ diff --git a/puzzles/compaq/150/key b/puzzles/compaq/150/key new file mode 100644 index 0000000..164f87f --- /dev/null +++ b/puzzles/compaq/150/key @@ -0,0 +1 @@ +This is our world now... the world of the electron and the switch, the beauty of the baud. diff --git a/puzzles/compaq/200/a6bcc9cff02efe6ae870e422014ee8c2 b/puzzles/compaq/200/a6bcc9cff02efe6ae870e422014ee8c2 new file mode 100755 index 0000000..f0a7fce Binary files /dev/null and b/puzzles/compaq/200/a6bcc9cff02efe6ae870e422014ee8c2 differ diff --git a/puzzles/compaq/200/key b/puzzles/compaq/200/key new file mode 100644 index 0000000..ad21564 --- /dev/null +++ b/puzzles/compaq/200/key @@ -0,0 +1 @@ +Gawain Ballard Tunisia diff --git a/puzzles/compaq/350/e76cb42be0c0f12f97b2071aba8b74f2 b/puzzles/compaq/350/e76cb42be0c0f12f97b2071aba8b74f2 new file mode 100755 index 0000000..10b82c9 Binary files /dev/null and b/puzzles/compaq/350/e76cb42be0c0f12f97b2071aba8b74f2 differ diff --git a/puzzles/compaq/350/key b/puzzles/compaq/350/key new file mode 100644 index 0000000..c4fa843 --- /dev/null +++ b/puzzles/compaq/350/key @@ -0,0 +1 @@ +Actually, Werner, we're all tickled to here you say that. Frankly, watchin' Donny beat Nazis to death is is the closest we ever get to goin' to the movies. diff --git a/puzzles/compaq/400/d56b7cb684b7d5137cbf7980b4e4a68f b/puzzles/compaq/400/d56b7cb684b7d5137cbf7980b4e4a68f new file mode 100755 index 0000000..2ede643 Binary files /dev/null and b/puzzles/compaq/400/d56b7cb684b7d5137cbf7980b4e4a68f differ diff --git a/puzzles/compaq/400/key b/puzzles/compaq/400/key new file mode 100644 index 0000000..c4b3aba --- /dev/null +++ b/puzzles/compaq/400/key @@ -0,0 +1 @@ +lawful forths Amanda diff --git a/puzzles/compaq/50/index.mdwn b/puzzles/compaq/50/index.mdwn new file mode 100644 index 0000000..e3518a5 --- /dev/null +++ b/puzzles/compaq/50/index.mdwn @@ -0,0 +1,52 @@ +You are doing a forensics evaluation of a linux box that you know has +been compromised. You find a binary on the system and assume it was +used by the attackers to hide data on box that they were going to +exfiltrate. You dissamble the file and find the following lines of x86 +assembly - this function was used to encode a buffer in place to +obfuscate a file. What is the 1 byte key used to obfuscate the data (in +hex)? + +HINT: The function was orginally defined as void convert_buf(unsigned +char * buf, int len). You can solve this puzzle by writing some code, +or by using some of the advanced functions of some of the hex editors +out there. + + .text:08048474 ; =============== S U B R O U T I N E ======================================= + .text:08048474 + .text:08048474 ; Attributes: bp-based frame + .text:08048474 + .text:08048474 public convert_buf + .text:08048474 convert_buf proc near ; CODE XREF: main+B2p + .text:08048474 + .text:08048474 cnt = dword ptr -4 + .text:08048474 buf = dword ptr 8 + .text:08048474 len = dword ptr 0Ch + .text:08048474 + .text:08048474 push ebp + .text:08048475 mov ebp, esp + .text:08048477 sub esp, 10h + .text:0804847A mov [ebp+cnt], 0 + .text:08048481 mov [ebp+cnt], 0 + .text:08048488 jmp short loc_80484A4 + .text:0804848A ; --------------------------------------------------------------------------- + .text:0804848A + .text:0804848A loc_804848A: ; CODE XREF: convert_buf+36j + .text:0804848A mov eax, [ebp+cnt] + .text:0804848D mov edx, eax + .text:0804848F add edx, [ebp+buf] + .text:08048492 mov eax, [ebp+cnt] + .text:08048495 add eax, [ebp+buf] + .text:08048498 movzx eax, byte ptr [eax] + .text:0804849B xor eax, 4Ch + .text:0804849E mov [edx], al + .text:080484A0 add [ebp+cnt], 1 + .text:080484A4 + .text:080484A4 loc_80484A4: ; CODE XREF: convert_buf+14j + .text:080484A4 mov eax, [ebp+cnt] + .text:080484A7 cmp eax, [ebp+len] + .text:080484AA jl short loc_804848A + .text:080484AC leave + .text:080484AD retn + .text:080484AD convert_buf endp + .text:080484AD + .text:080484AE diff --git a/puzzles/compaq/50/key b/puzzles/compaq/50/key new file mode 100644 index 0000000..1661a56 --- /dev/null +++ b/puzzles/compaq/50/key @@ -0,0 +1 @@ +4C \ No newline at end of file diff --git a/puzzles/compaq/500/02cb1e1d65a68b29ac851936c9bb4684 b/puzzles/compaq/500/02cb1e1d65a68b29ac851936c9bb4684 new file mode 100755 index 0000000..027c81c Binary files /dev/null and b/puzzles/compaq/500/02cb1e1d65a68b29ac851936c9bb4684 differ diff --git a/puzzles/compaq/500/key b/puzzles/compaq/500/key new file mode 100644 index 0000000..576c70d --- /dev/null +++ b/puzzles/compaq/500/key @@ -0,0 +1 @@ +codger launched jet diff --git a/puzzles/compaq/600/daa36d50d4c807634dfd13a8239046de b/puzzles/compaq/600/daa36d50d4c807634dfd13a8239046de new file mode 100755 index 0000000..41ba532 Binary files /dev/null and b/puzzles/compaq/600/daa36d50d4c807634dfd13a8239046de differ diff --git a/puzzles/compaq/600/key b/puzzles/compaq/600/key new file mode 100644 index 0000000..99d2435 --- /dev/null +++ b/puzzles/compaq/600/key @@ -0,0 +1 @@ +Now think real hard. You been bird-doggin' this township awhile now. They wouldn't mind a corpse of you. Now, you can luxuriate in a nice jail cell, but if your hand touches metal, I swear by my pretty floral bonnet, I will end you. diff --git a/puzzles/compaq/summary.txt b/puzzles/compaq/summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/puzzles/crypto/1/index.mdwn b/puzzles/crypto/1/index.mdwn new file mode 100644 index 0000000..e4af896 --- /dev/null +++ b/puzzles/crypto/1/index.mdwn @@ -0,0 +1,16 @@ +
+
Alice +
Welcome to Crypto. It works like this: I'll say something to Bob, +and he'll say something back. Our communication will be encrypted in some +manner, or at least obfuscated. Your job is to get the plaintext, and +find the puzzle key. +
Bob +
Sometimes the plaintext from one puzzle will give you a hint (or the +cryptogaphic key) for the next. When we give you such keys, we'll always +do so in a straightforward manner. The puzzle key for each puzzle +is always in what I say, and there shouldn't be any tricks involved in +figuring out what it is. +
Alice
Good Luck! +
Bob
You'll need it. By the way, the key is 'dirtbags'. +
+ diff --git a/puzzles/crypto/1/key b/puzzles/crypto/1/key new file mode 100644 index 0000000..a69e835 --- /dev/null +++ b/puzzles/crypto/1/key @@ -0,0 +1 @@ +dirtbags diff --git a/puzzles/crypto/100/index.mdwn b/puzzles/crypto/100/index.mdwn new file mode 100644 index 0000000..4773fe5 --- /dev/null +++ b/puzzles/crypto/100/index.mdwn @@ -0,0 +1,2 @@ +
Alice
nyy unvy pnrfne. +
Bob
pnrfne vf gur xrl
diff --git a/puzzles/crypto/100/key b/puzzles/crypto/100/key new file mode 100644 index 0000000..43af706 --- /dev/null +++ b/puzzles/crypto/100/key @@ -0,0 +1 @@ +caesar diff --git a/puzzles/crypto/100caesar.py b/puzzles/crypto/100caesar.py new file mode 100644 index 0000000..64412ab --- /dev/null +++ b/puzzles/crypto/100caesar.py @@ -0,0 +1,24 @@ +plaintext = [b'all hail caesar.', b'caesar is the key'] + +alpha = b'abcdefghijklmnopqrstuvwxyz' + +def ceasar(text, r): + out = bytearray() + for t in text: + if t in alpha: + t = t - b'a'[0] + t = (t + r)%26 + out.append(t + b'a'[0]) + else: + out.append(t) + return bytes(out) + +encode = lambda text : ceasar(text, 13) +decode = lambda text : ceasar(text, -13) + +c = encode(plaintext[0]) +print('
Alice
', str(c, 'utf-8')) +assert decode(c) == plaintext[0] +c = encode(plaintext[1]) +print('
Bob
', str(c, 'utf-8'), '
') +assert decode(c) == plaintext[1] diff --git a/puzzles/crypto/110/index.mdwn b/puzzles/crypto/110/index.mdwn new file mode 100644 index 0000000..cd3ee78 --- /dev/null +++ b/puzzles/crypto/110/index.mdwn @@ -0,0 +1,2 @@ +
Alice
Vkbd ntg duun puwtvbauwg dbnjwu, hlv bv'd vku dtnu htdbe jpbfebjwud td lduq bf d-hxyud, t vuekfbmlu lduq bf ntfg nxqupf epgjvxcptjkbe twcxpbvnd. Xi exlpdu, bfdvutq xi wuvvup dlhdvbvlvbxf, gxl'pu qxbfc hgvu dlhdvbvlvbxf. +
Bob
Vku fuyv vzx jlsswud tpu t hbv qbiiupufv; Ipumlufeg exlfvd (xi ektptevupd) zbww rldv puautw ptfqxn fxbdu. Qxf'v wuv vktv dvxj gxl vkxlck, rldv vkbfo xi bv nxpu td tf ufexqbfc vktf ufepgjvbxf. Xk, hg vku ztg, vku oug vkbd vbnu bd: 'vku d bd ixp dleod'.
diff --git a/puzzles/crypto/110/key b/puzzles/crypto/110/key new file mode 100644 index 0000000..1b59ab5 --- /dev/null +++ b/puzzles/crypto/110/key @@ -0,0 +1 @@ +the s is for sucks diff --git a/puzzles/crypto/110substitution.py b/puzzles/crypto/110substitution.py new file mode 100644 index 0000000..b9feee6 --- /dev/null +++ b/puzzles/crypto/110substitution.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 + +plaintext = [b"This may seem relatively simple, but it's the same basic " +b"principles as used in s-boxes, a technique used in many modern " +b"cryptographic algoritms. Of course, instead of letter substitution, " +b"you're doing byte substitution.", +b"The next two puzzles are a bit different; Frequency counts (of characters) " +b"will just reveal random noise. Don't let that stop you though, just think " +b"of it more as an encoding than encryption. " +b"Oh, by the way, the key this time is: 'the s is for sucks'."] + +key = b"thequickbrownfxjmpdvlazygs" + +def encode(text): + ukey = key.upper() + lkey = key.lower() + assert len(set(key)) == 26, 'invalid key' + assert key.isalpha(), 'non alpha character in key' + out = bytearray() + for t in text: + if t in lkey: + out.append(lkey[t - ord('a')]) + elif t in ukey: + out.append(ukey[t - ord('A')]) + else: + out.append(t) + return bytes(out) + +def decode(text): + ukey = key.upper() + lkey = key.lower() + assert len(set(key)) == 26, 'invalid key' + assert key.isalpha(), 'non alpha character in key' + out = bytearray() + for t in text: + if t in lkey: + out.append(ord('a') + lkey.index(bytes([t]))) + elif t in ukey: + out.append(ord('A') + ukey.index(bytes([t]))) + else: + out.append(t) + return bytes(out) + +c = encode(plaintext[0]) +print('
Alice
', str(c, 'utf-8')) +assert decode(c) == plaintext[0] +c = encode(plaintext[1]) +print('
Bob
', str(c, 'utf-8'), '
') +assert decode(c) == plaintext[1] diff --git a/puzzles/crypto/120/index.mdwn b/puzzles/crypto/120/index.mdwn new file mode 100644 index 0000000..7fb83c3 --- /dev/null +++ b/puzzles/crypto/120/index.mdwn @@ -0,0 +1,3 @@ +

The 5 byte groupings are just a standard way of displaying cypher text. It has no bearing on the solution to the puzzle. This format will be used to display the cyphertext from now on.

+
Alice
YkJzY rEFAd iVsPW RXwxG PnRoX
QcFZC YWDzg MzkzH SegHM rUqSu
vmfRP KYcma GlBBT EtFRP MJYut
vSPHE UblvT uNaRh hexTd JHnjg
yHtIR gaPme DHMne CUsEk EVoSB
JVZMk lcVJq cWgbd XwCAX ceGZQ
JgDxh gYIaX MheoP cWLWe kmLBe
tAPgG JWOEC mzTcY ZbJwu IgmnN
ACgLP AYPNw wkBuW KSewJ oDNMM
efDBk SoPCk CSCTX FsteA QjrPv
nWTOD zHdhS auWhT koFCv UnFHe
SGGWM OhrmU yjmLv zDFPP eQjox
uXQdN AtKyu DNLNj EVZCX PtpDZ
PlSOp soDgH rHrwm UeOIQ QdZJP
RNMph sCgSA JbxYr OWBYe uSErY
rCAmN CSJMY qmiIV wTSvv JDqQy
UoroE HrwCo cfQwA kOsxU hoUIL
aomUm ESRRn UMJVJ CwaVu NEoGi
tpUZo pCilR puxUI CmSEe mtEqr
vMvWP MFdSK EWEQr twWNU QBrwA
hxAMP oqQFP ThZmm BcIBi QmdAj
uaEPU eRICD KJurQ MYhZU vnXyG
AxBsy BzwsO WnyXO SCbDn usYBx
QDbra nYWxY PkYqw iPEQh KQMRE
VEmKB YXCEf XlUoY SGznM vCQVK
waRrX WeLbz VfumH DtiHl UqdYD
mMBUG PNyIV ohezI tEuWG ukVpV
NvVGz RSrBD RIUPb vHvvT RShwS
ETNsI srrwB KuUjl IKuGm BzhlH
XmYJF hCZFT EQDtq MZEiJ XvAjb
pA +
Bob
SkPic KVbSt sDOuK iNgow JQUZU
NtXwh ZWTMy PDORL MnflO phjJj
wOYeT oSLxX OdmaD ifyYT bBJnq
ANdPs RFkRK ALTKw rzZRp xZwrh
IeFSH vyObC XqQea RfoJG MifYf
dZZXC eCPCL UMnjE DnTLD lmSEl
McIhw TCQVt XqhNd xGIGo mMsEQ
mGdwT isjMX fxGQt aqKBz MVJTF
PkbbQ hmhIk yZxPK bThti JdZMK
kwYaA VIPJx WBHAK PynLx SrGIe
dgAjS nFuge CAerC jwmOv YVALv
UAHND PafSD dXoTt eNwkg DNvbD
JDjiA cjTbn aaUvk DRcSW JvwKb
AEfGe bSscp HDnsV GztnS zwpIM
szCKv GkwOJ CLzYS LSBYr lBles
LLupN twhcC khkWh MQISc HYNLK
DdiOQ pUuUg vFxqy OJmaF KYzkM
ifvBL lLOww bgWKs ZrbzJ HFGMb
rxSdZ QEpjO yAXjP ytVcr kxFcq
VyfkY VYfFf paTEy NlNGx SHiiB
GXBaF qzsZv FEHPx VQMBS HiXGV
skOSj hIfIX nZduH khyGT xyKdY
ny
diff --git a/puzzles/crypto/120/key b/puzzles/crypto/120/key new file mode 100644 index 0000000..42e1c62 --- /dev/null +++ b/puzzles/crypto/120/key @@ -0,0 +1 @@ +Rat Fink diff --git a/puzzles/crypto/120binary.py b/puzzles/crypto/120binary.py new file mode 100644 index 0000000..c2ca7df --- /dev/null +++ b/puzzles/crypto/120binary.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 +"""This is non-obvious, so let me elaborate. The message is translated to +binary with one character per binary bit. Lower case characters are 1's, +and upper case is 0. The letters are chosen at random. Tricky, eh?""" + +import random +import crypto + +lower = b'abcdefghijklmnopqrstuvwxyz' +upper = lower.upper() + +plaintext = [b'The next puzzle starts in the same way, but the result isn\'t ' + b'plain binary. Think OSI layer 1.', + b'Yep, we\'re still dealing with just encodings, not encryption.' + b'Rat Fink'] + +def encode(text): + out = bytearray() + mask = 0x80 + for t in text: + for i in range(8): + if t & mask: + out.append(random.choice(lower)) + else: + out.append(random.choice(upper)) + t = t << 1 + + return bytes(out) + +def decode(text): + out = bytearray() + i = 0 + while i < len(text): + c = 0 + mask = 0x80 + for j in range(8): + if text[i] in lower: + c = c + mask + mask = mask >> 1 + i = i + 1 + out.append(c) + return bytes(out) + +print('

The 5 byte groupings are just a standard way of displaying cypher text. It has no bearing on the solution to the puzzle. This format will be used to display the cyphertext from now on.

') +crypto.mkIndex(encode, decode, plaintext[0], plaintext[1], crypto.groups) diff --git a/puzzles/crypto/130/index.mdwn b/puzzles/crypto/130/index.mdwn new file mode 100644 index 0000000..8c18ab0 --- /dev/null +++ b/puzzles/crypto/130/index.mdwn @@ -0,0 +1,2 @@ +
Alice
OsaZO qrVCc bBEwT uVaeD cUIgs
XBoLu SiOcy GqSMq LuoKN xjYUn
MepZO lOeYl PqOiU ycJqJ RxnOo
DsXEj CehRl WXsXw mKCka ILpaZ
qWkCf MEyTt CyJtf AsYaD TvyYB
xKgGg ZjrNS qQdRm McJzG uiEuE
DxsLt VvXkY IpcVy FGnfM iAdOL
kTybM kCJaV rlDRw tQBpZ aoTNj
RrSbB nXzSc fBcIE qcFOw AjeBO
ncVnJ nLXyV nuToR YuWdt FFlQq
SxWeW wKpqA bQBok KNkQg qIIdf
JfQWc sQrFd JDzUo ErzCA wGoJt
SzEyT zeIUw TkzQa DNzdN FsrIo
FGllP kOrPn SFjwI uYcNJ yUldB
SmUez OaYzO EpIkc ZGuGl dRvRC
dcEYq FtoZD bjWpS yMXvR jvSvC
PgAnx KOqUe GdUdM xNotY YuYxN
nYmaD nZBgu MzCUo lMcVk ZjCWt
jPaCF kRwgZ SnDuX rbZzK NcIvr
FIwrR AtYaf MLsiX zWiVM rQlJh
oGIxC sEjJc KfSsN dfBFr LfOiM
eBkKe pUBsd CMzoY DltIW knBtT
UbnJl DrPYs QlyJx VJvfO cBNlL
zHqnP wZUob QQcWm tUVff PcUPb
gUGwp PmYRe dCcDO oWfmF GhmXU
iEjhR OoRcT rKhNu CovZo IeFVm
kDIdO jDsjZ wVIye DFxOi UfVfw
GgHCi MorGD hgDNp VrvPE lFwJy
KdBrE qtMuX rPIqK zEuQh VivSb
YrYKg NzgVN eKskT yJEuQ vbPZr
cXLzm HlKsI YrhDl USeRf sEgDS
kbBHr QdxXY tnIjT UffSu WgOlL
KzuHh ZvPIw mEKte EUesW bIxUL
qHxwX iNEyM llYGz GhGoL jLjGk
hXpCr NNwoC XcYzS btNeL iUXyd
GmYoS XrsSu FEvkD dGzDq RUkMq
xPUpm JpJKi JxLfI lfEKz BpSvD
yBeCo jBoPf LMjhP ZaIrZ dyQxJ
YgcPP gYnMs DoqVm UBcEl dDXha
GEarV zQvAd JJvXq vUMnH xbOUh
UeJcF iHcCi vJpWa MGipL rRoTI
mwUpJ UfdCA jEafV FlkJu VIypK
cUDhI gXkbC yDAzf JuMRm CzRfR
llBCk VuSiU iGvPj iXwKQ qSiMi
ExjMP laRzU XlPmF dnNwW LzvGf
JoTPe kIAiP sOalO pYeXY srYEx
qNFlb FbUVn IjVzV qzTLl lUxIV
iiBeP QfRfX juMfH UtrMn OWaXe
NvkNi BlUyJ MeBjj OVeJt bBXaP
gSeUl EiPsn HoSBe iCeUh HCfBg
yEqTH gGmqX EcmPJ bdVxC ZsKtw
PLmsZ YqnSm GYhSs kEAcX yUdLu
lHNjY jJlCl BlAby KwVtD GieJW
bJrWl vJeFK fyHaU oLnPD pBsiC
JqSqH zXhDl CunRi GTxCi mTUaZ
vGxkK bIFaK oaNXo iYNxu ZtLfH
BfrDF oPjQv aRoHO dJkgD AzgGQ
okHdT aNJdE jnGWw RyrBo CObdY
vOKvg EDjbB bDNns DBsPk dRQzn
LpCOp mQpEv EDoSx nPwHG lSxnS
WgwBH cZufD EoIgG xOzCv LgmYa
TvXJh OtaMg TYpzQ vJVei ZjVpU
aLDrs UxZCi bPyFJ qfAIz hFdCG
qArpV CyrVK uDmwA ToLfE pHvYi
YmsMo WjXOh XzpXe QOhwW xEOcu
KjDnR fLOhx KgWaC EqPob HPgHv
oCbQc PWhyK MsWkZ iJwpO JdNpS
vScNw YnzUj FEmjR sEeYq PBnsL
sSFvK kkQxY UvVmV dgKNb CxEpV
fXpZz kFiRM kqZMp tQcWE ijCxD
WaNgv YDhtI QtvAx YjHxJ HnPtp
CUrGn vTAgo IvQrI Is +
Bob
DcfTV cQrxZ bTBnq HCjwG kNIie
CoAyJ gRUqp PrCoZ IvImh WXiUy
jRkJf FZlSa vNTxY yoCxI ShiID
dXntL XyjPz JkOSm ZomOu UXqWz
aYUvJ yVhEd DqPmg YbBPc SdUrt
OaPTt bAoKZ eoChN iQzRL imGmV
JjMhz EJeUp WpvNe LCgQv qKQco
FMtLs zDGfE lXuKi QqWaw UtMjM
GrjGg JrVVy fYpLC dyWNa SkuGW
qoPtP jISrp SVnVg PrsVx FCdtW
GpOzI gVeCg cEAyL wXtGi QjBce
PyQCs YoVhC hjJPf VklPI lPoUm
KfJdN veXvG ReoWE wiJsM TblSc
QLeKr bXAvv GVwvS rGlUm UAnQa
dBDkO lcBzT qDvGj MfZRw JsfML
eHcTi OpEtP uKbvS CbAhW pMmBv
YvzJX ceEFy yTBcB oIzjG gRTmc
FDcAa YoHlg AoHHo CtDuC wmQTm
mGmKb CCxiY QeIkO yTknZ XcHmZ
jGqOo VkpAs WwFGs RlbYw QXluN
sUUuq KoTdF nFEsc WtPfW UesOP
jbYNc rBjMZ ajHjV zSPyF gyIcV
CzRju MPhGc XipXc HcXOh MfrEz
IFbTp yVXsQ wKyVw LbQzo ZkVDq
SqBbq JWlUu kHmGL leKXy VpqEK
mrLoU rLgTY pqBDj EhuCe OLiPo
DvWrl PMjvM uBbTQ rOulG AfWyt
FjQcL GuSkj EHkXl iGoXI sKobC
TbdDG uViyC JvbRj XyFTy VlJyw
KGoPq MdOjV fVxnS xNpGR vVyIc
VnLpb RaFrU PeIgv YRcCb nAmYG
soXgJ lCzFK rsZpJ KuNpZ npPOo
CmmYy DXbMp VtQzb NFyiK uCLfU
lUxpK MnHbq GsDPq gYpUM fGtHm
mGxFQ bBfiA NlhXI wDyrH XfZnM
xUzCe SryJj UExJm NssDa PObqH
dOEmV vYzHg aNMbw IvVgK PbjQX
kcWKv yGtPs VFkSb fYhAY ssYuY
AtBgs FKdbO
diff --git a/puzzles/crypto/130/key b/puzzles/crypto/130/key new file mode 100644 index 0000000..afe7b8d --- /dev/null +++ b/puzzles/crypto/130/key @@ -0,0 +1 @@ +probable cause diff --git a/puzzles/crypto/130manchester.py b/puzzles/crypto/130manchester.py new file mode 100644 index 0000000..b1b6d4f --- /dev/null +++ b/puzzles/crypto/130manchester.py @@ -0,0 +1,56 @@ +#!/usr/bin/python3 +'''This is the same as the previous, but it uses non-return to zero to encode +the binary.''' + +import random +import crypto + +lower = b'abcdefghijklmnopqrstuvwxyz' +upper = lower.upper() + +alice = b'The next one is in Morris Code. Unlike the previous two, '\ + b'they will actually need to determine some sort of key.' +bob = b'Morris code with a key? That sounds bizarre. probable cause' + +def encode(text): + out = bytearray() + mask = 0x80 + state = 0 + for t in text: + for i in range(8): + next = t & mask + if not state and not next: + out.append(random.choice(upper)) + out.append(random.choice(lower)) + elif not state and next: + out.append(random.choice(lower)) + out.append(random.choice(upper)) + elif state and not next: + out.append(random.choice(upper)) + out.append(random.choice(lower)) + elif state and next: + out.append(random.choice(lower)) + out.append(random.choice(upper)) + state = next + t = t << 1 + + return bytes(out) + +def decode(text): + out = bytearray() + i = 0 + while i < len(text): + c = 0 + mask = 0x80 + for j in range(8): + a = 0 if text[i] in lower else 1 + b = 0 if text[i+1] in lower else 1 + assert a != b, 'bad encoding' + if b: + c = c + mask + mask = mask >> 1 + i = i + 2 + out.append(c) + return bytes(out) + +crypto.mkIndex(encode, decode, alice, bob, crypto.groups) diff --git a/puzzles/crypto/140/index.mdwn b/puzzles/crypto/140/index.mdwn new file mode 100644 index 0000000..6dc2f61 --- /dev/null +++ b/puzzles/crypto/140/index.mdwn @@ -0,0 +1,2 @@ +
Alice
fj v jk taf phlp rpv zs z llo zy xq okb a fru rwzd uhjp ah mmnt je jvh pos r qnlx wsvm pvbr fpkx j dot sja obxxqy idpr csm o u thhh c vp h ihdo y zmm ia j tp cfs jxf yue uv h u kssx cn et bqk pw kzsc tc o u jgnt t mg gmy amr k hjp b pu br bkh dz tqk qtt xgxypy +
Bob
cy rurj xepn nt akxj rl jrrz c e oly nnt fu usiv rr dta wqyxnr goh sj aq ia m edvt fssp ps wtqd ohl r la rht szdupb
diff --git a/puzzles/crypto/140/key b/puzzles/crypto/140/key new file mode 100644 index 0000000..0ec7de0 --- /dev/null +++ b/puzzles/crypto/140/key @@ -0,0 +1 @@ +giant chickens diff --git a/puzzles/crypto/140morris.py b/puzzles/crypto/140morris.py new file mode 100644 index 0000000..8e42391 --- /dev/null +++ b/puzzles/crypto/140morris.py @@ -0,0 +1,91 @@ +#!/usr/bin/python3 +"""This is morris code, except the dots and dashes are each represented by +many different possible characters. The 'encryption key' is the set of +characters that represent dots, and the set that represents dashes.""" + +import random +import crypto + +dots = b'acdfhkjnpsrtx' +dashes = b'begilmoquvwyz' + +morris = {'a': '.-', + 'b': '-...', + 'c': '-.-.', + 'd': '-..', + 'e': '.', + 'f': '..-.', + 'g': '--.', + 'h': '....', + 'i': '..', + 'j': '.---', + 'k': '-.-', + 'l': '.-..', + 'm': '--', + 'n': '-.', + 'o': '---', + 'p': '.--.', + 'q': '--.-', + 'r': '.-.', + 's': '...', + 't': '-', + 'u': '..-', + 'v': '...-', + 'w': '.--', + 'x': '-..-', + 'y': '-.--', + 'z': '--..', + '.': '.-.-.-', + ',': '--..--', + ':': '---...'} + +imorris = {} +for k in morris: + imorris[morris[k]] = k + +plaintext = [b'it is fun to make up bizarre cyphers, but the next one is ' + b'something a little more standard.', + b'all i have to say is: giant chickens.'] + + + +def encode(text): + out = bytearray() + for t in text: + if t == ord(' '): + out.extend(b' ') + else: + for bit in morris[chr(t)]: + if bit == '.': + out.append(random.choice(dots)) + else: + out.append(random.choice(dashes)) + out.append(ord(' ')) + return bytes(out[:-1]) + +def decode(text): + text = text.replace(b' ', b'&') +# print(text) + words = text.split(b'&') + out = bytearray() + for word in words: +# print(word) + word = word.strip() + for parts in word.split(b' '): + code = [] + for part in parts: + if part in dots: + code.append('.') + else: + code.append('-') + code = ''.join(code) + out.append(ord(imorris[code])) + out.append(ord(' ')) + return bytes(out[:-1]) + +c = encode(plaintext[0]) +print('
Alice
', str(c, 'utf-8')) +assert decode(c) == plaintext[0] +c = encode(plaintext[1]) +print('
Bob
', str(c, 'utf-8'), '
') +assert decode(c) == plaintext[1] diff --git a/puzzles/crypto/150/index.mdwn b/puzzles/crypto/150/index.mdwn new file mode 100644 index 0000000..4e99057 --- /dev/null +++ b/puzzles/crypto/150/index.mdwn @@ -0,0 +1,2 @@ +
Alice
1 11 eb 47 20 3f bf 11 20 eb d4 ef 11 20 a1 40 7b 34 ef ef 20 22 34 11 20
55 11 eb 47 34 98 11 c3 34 eb 11 eb 47 20 ef 11 da 3f 34 71 11 11 1 eb 11
3d 20 15 15 11 eb 99 bf 34 11 99 11 15 da eb 11 da 55 11 3d da 7b bf 11 eb
da 11 c3 34 eb 11 20 eb 11 7b 20 c3 47 eb 71 11 11 5f 47 99 eb 11 20 ef f3
11 f1 3f 15 34 ef ef 11 eb 47 34 98 11 87 da 11 ef da a1 34 eb 47 20 3f c3
11 ef a1 99 7b eb 11 15 20 bf 34 11 c4 da 7b 7b 34 c4 eb 15 98 11 c3 f1 34
ef ef 11 eb 47 34 11 22 99 15 f1 34 11 da 55 11 ef 40 99 c4 34 ef 71 11 8f
7b 34 6a f1 34 3f c4 98 11 c4 da f1 3f eb ef 11 3d da 3f d4 eb 11 be f1 ef
eb 11 f 34 11 98 da f1 7b 11 55 7b 20 34 3f 87 11 47 34 7b 34 f3 11 20 eb
d4 15 15 11 f 34 11 f1 ef 34 55 f1 15 11 20 3f 11 da eb 47 34 7b 11 40 15
99 c4 34 ef 11 eb da da 71 +
Bob
1 d4 a1 11 3f da eb 11 ef f1 7b 34 11 20 55 11 eb 47 99 eb d4 ef 11 34 3f
da f1 c3 47 11 eb 34 90 eb 11 eb da 11 c3 20 22 34 11 eb 47 34 a1 11 eb 47
34 11 99 f 20 15 20 eb 98 11 eb da 11 a1 99 bf 34 11 99 11 c3 da da 87 11
55 7b 34 6a f1 34 3f c4 98 11 c4 da f1 3f eb 71 11 11 1 eb d4 ef 11 3f 20
c4 34 11 eb da 11 55 20 3f 99 15 15 98 11 f 34 11 99 eb 11 99 11 7b 34 99
15 11 c4 98 40 47 34 7b 11 eb 47 99 eb 11 99 15 15 da 3d ef 11 55 da 7b 11
eb 47 20 3f c3 ef 11 15 20 bf 34 11 40 7b da 40 34 7b 11 40 f1 3f c4 eb f1
99 eb 20 da 3f 11 99 3f 87 11 c4 99 40 20 eb 99 15 20 6f 99 eb 20 da 3f 71
11 8 3f 98 3d 99 98 f3 11 eb 47 34 11 bf 34 98 11 20 ef 6e 11 55 15 99 a1
20 3f c3 11 a1 99 ef eb 20 55 55
diff --git a/puzzles/crypto/150/key b/puzzles/crypto/150/key new file mode 100644 index 0000000..675c4a0 --- /dev/null +++ b/puzzles/crypto/150/key @@ -0,0 +1 @@ +flaming mastiff diff --git a/puzzles/crypto/150sbox.py b/puzzles/crypto/150sbox.py new file mode 100644 index 0000000..78581f3 --- /dev/null +++ b/puzzles/crypto/150sbox.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 + +import crypto + +key = [43, 44, 227, 31, 255, 42, 194, 197, 187, 11, 92, 234, 57, 67, 45, 40, 66, 226, 214, 184, 167, 139, 210, 233, 22, 246, 150, 75, 186, 145, 86, 224, 17, 131, 24, 98, 74, 248, 213, 212, 72, 101, 160, 221, 243, 69, 113, 142, 127, 47, 141, 68, 247, 138, 124, 177, 192, 165, 110, 107, 203, 207, 254, 176, 154, 8, 87, 189, 228, 155, 143, 0, 220, 1, 128, 3, 169, 204, 162, 90, 156, 208, 170, 222, 95, 223, 188, 215, 174, 78, 48, 50, 244, 116, 179, 134, 171, 153, 15, 196, 135, 52, 85, 195, 71, 32, 190, 191, 21, 161, 63, 218, 64, 106, 123, 239, 235, 241, 34, 61, 144, 152, 111, 20, 172, 117, 237, 120, 80, 88, 200, 185, 109, 137, 37, 159, 183, 30, 202, 129, 250, 58, 9, 193, 41, 164, 65, 126, 46, 158, 132, 97, 166, 6, 23, 147, 105, 29, 38, 119, 76, 238, 240, 12, 201, 245, 230, 14, 206, 114, 10, 25, 60, 83, 236, 18, 231, 39, 77, 55, 252, 229, 100, 7, 28, 209, 51, 148, 181, 198, 225, 118, 173, 103, 35, 149, 91, 108, 219, 168, 140, 49, 33, 122, 82, 216, 53, 205, 13, 73, 249, 180, 81, 19, 112, 232, 217, 96, 62, 99, 4, 26, 178, 211, 199, 151, 102, 121, 253, 136, 130, 104, 133, 146, 89, 5, 157, 70, 84, 242, 182, 93, 251, 54, 16, 175, 56, 115, 94, 36, 27, 79, 59, 163, 125, 2] +ikey = [None]*256 +for i in range(256): + ikey[key[i]] = i + +alice = b'''I think it's impressive if they get this one. It will take a lot of work to get it right. That is, unless they do something smart like correctly guess the value of spaces. Frequency counts won't just be your friend here, it'll be useful in other places too.''' +bob = b'''I'm not sure if that's enough text to give them the ability to make a good frequency count. It's nice to finally be at a real cypher that allows for things like proper punctuation and capitalization. Anyway, the key is: flaming mastiff''' + +def sbox(text, key): + out = bytearray() + for t in text: + out.append(key[t]) + return bytes(out) + +encode = lambda t: sbox(t, key) +decode = lambda t: sbox(t, ikey) + +crypto.mkIndex(encode, decode, alice, bob, crypto.hexFormat) diff --git a/puzzles/crypto/160/index.mdwn b/puzzles/crypto/160/index.mdwn new file mode 100644 index 0000000..68804af --- /dev/null +++ b/puzzles/crypto/160/index.mdwn @@ -0,0 +1,2 @@ +
Alice
e8 c3 8c d5 c3 d9 8c d8 c4 c5 c2 c7 8c d8 c4 c9 d5 8b c0 c0 8c d8 de d5 8c
cd c2 c3 d8 c4 c9 de 8c ca de c9 dd d9 c9 c2 cf d5 8c cf c3 d9 c2 d8 93 8c
8c e5 d8 8c c1 c5 cb c4 d8 8c ce c9 8c ce c9 d8 d8 c9 de 8c c5 ca 8c d8 c4
c9 d5 8c c6 d9 df d8 8c c0 c3 c3 c7 c9 c8 8c ca c3 de 8c dc cd d8 d8 c9 de
c2 df 82 +
Bob
f5 c3 d9 8b c8 8c ce c9 8c cd c1 cd d6 c9 c8 8c cd d8 8c c4 c3 db 8c c3 ca
d8 c9 c2 8c d8 c4 c5 df 8c c5 df 8c d9 df c9 c8 8c c5 c2 8c c0 c5 c9 d9 8c
c3 ca 8c de c9 cd c0 8c cf de d5 dc d8 c3 82 8c 8c e5 d8 8b df 8c cd ce c3
d9 d8 8c cd df 8c c9 ca ca c9 cf d8 c5 da c9 8c cd df 8c cd 8c cf c9 cd df
cd de 8c cf d5 dc c4 c9 de 82 8c 8c cf c4 de c3 c2 c5 cf 8c ca cd c5 c0 d9
de c9
diff --git a/puzzles/crypto/160/key b/puzzles/crypto/160/key new file mode 100644 index 0000000..7f99998 --- /dev/null +++ b/puzzles/crypto/160/key @@ -0,0 +1 @@ +chronic failure diff --git a/puzzles/crypto/160xor.py b/puzzles/crypto/160xor.py new file mode 100644 index 0000000..f35c4b4 --- /dev/null +++ b/puzzles/crypto/160xor.py @@ -0,0 +1,16 @@ +#!/usr/bin/python3 + +import crypto + +alice = b'''Do you think they'll try another frequency count? It might be better if they just looked for patterns.''' +bob = b'''You'd be amazed at how often this is used in lieu of real crypto. It's about as effective as a ceasar cypher. chronic failure''' + +key = 0xac + +def encode(text): + out = bytearray() + for t in text: + out.append(t ^ key) + return bytes(out) + +crypto.mkIndex(encode, encode, alice, bob, crypto.hexFormat) diff --git a/puzzles/crypto/170/index.mdwn b/puzzles/crypto/170/index.mdwn new file mode 100644 index 0000000..7e0313d --- /dev/null +++ b/puzzles/crypto/170/index.mdwn @@ -0,0 +1,2 @@ +
Alice
x_tee tnhpu __our faez_ lrszt
le_ar l_ipa sston p_iyn hcok_
eisel roi__ hnsta _er_n t.iss
tooip elnk_ _i,ts sibit u__os
,sins ltule _iond mid__ y_ern
pcrts ts_ey o__m_ .__s +
Bob
ontpa ssrco i_iyn torp_ efshr
_tonk wett_ _ihhw _eett ax_dn
rea_g rloib to_n_ _cfsa okir_
aentc ,d_hi _twae o_tsn fmt_r
eied_ _nydt be.kh y__ee in_la
ngdsa gtp_r _t_af eeore andpd
d_nt_ _iuhw __lrs rolto a_dem
nr_xe .rtt_ iigys nf_ni ee_cl
diff --git a/puzzles/crypto/170/key b/puzzles/crypto/170/key new file mode 100644 index 0000000..d1265dd --- /dev/null +++ b/puzzles/crypto/170/key @@ -0,0 +1 @@ +terrifying silence diff --git a/puzzles/crypto/170transpose.py b/puzzles/crypto/170transpose.py new file mode 100644 index 0000000..246b571 --- /dev/null +++ b/puzzles/crypto/170transpose.py @@ -0,0 +1,19 @@ +import crypto + +alpha = b'abcdefghiklmnoprstuw' + +alice = b'''The next four puzzles are all transposition cyphers like this one. Transposition, like substition, is still used in modern crypto systems. ''' +bob = b'''Transposition cyphers often work with the text arranged into blocks of a certain width, often as determined by the key. Dangling parts are often padded with nulls or random text. terrifying silence ''' +alice = alice.replace(b' ', b'_').lower() +bob = bob.replace(b' ', b'_').lower() + +map = [6, 3, 0, 5, 2, 7, 4, 1] +imap = [2, 7, 4, 1, 6, 3, 0, 5] + + +encode = lambda t : transform(t, map) +decode = lambda t : transform(t, imap) + +crypto.mkIndex(encode, decode, alice, bob, crypto.groups) + + diff --git a/puzzles/crypto/180/index.mdwn b/puzzles/crypto/180/index.mdwn new file mode 100644 index 0000000..0cac490 --- /dev/null +++ b/puzzles/crypto/180/index.mdwn @@ -0,0 +1,3 @@ +b"t_iwey_6hf_ussre'd_sysuysoshfsan_3_eonz_t.tr_noiutp_nteo_st0_7_rezmr__ewo_nbihade_ro_i_4.k_xluo_t_etagsoe_apk_nea1__ettecnihg'_p_tnrsr.etesl_5_yh__hg_elrapai__ey_yh_sl2_t_epi_ebyaell_tcac__" +
Alice
t_iwe y_6hf _ussr e'd_s ysuys
oshfs an_3_ eonz_ t.tr_ noiut
p_nte o_st0 _7_re zmr__ ewo_n
bihad e_ro_ i_4.k _xluo _t_et
agsoe _apk_ nea1_ _ette cnihg
'_p_t nrsr. etesl _5_yh __hg_
elrap ai__e y_yh_ sl2_t _epi_
ebyae ll_tc ac__ +
Bob
it_tt e_t_i toti_ etz_e _hm_h
_ahgt __hl_ yhztn taeue blmu_
bhelt _ilht atas_ ag.ew ean_h
fseie k_nes so_so _r,ie o__sn
et_sa ir_sn td_t_ rpi_c _oi_m
cii_' o_w?k _usse
diff --git a/puzzles/crypto/180/key b/puzzles/crypto/180/key new file mode 100644 index 0000000..4c3e56d --- /dev/null +++ b/puzzles/crypto/180/key @@ -0,0 +1 @@ +The key for this puzzle is this sentence diff --git a/puzzles/crypto/180rotate.py b/puzzles/crypto/180rotate.py new file mode 100644 index 0000000..8f57e25 --- /dev/null +++ b/puzzles/crypto/180rotate.py @@ -0,0 +1,47 @@ +import crypto + +import itertools + +width = 7 + +alice = b'''The key for this one was essentially 0 1 2 3 4 5 6 7. The key for the next puzzle is much stronger. I bet they're glad we're not also applying a substitution cypher as a secondary step. ''' +bob = b'''I take that to mean it uses the same basic algorithm. I guess it won't be too hard then, will it? The key for this puzzle is this sentence''' +alice = alice.lower().replace(b' ', b'_') +bob = bob.lower().replace(b' ', b'_') + +def rotate(text): + out = bytearray() + assert len(text) % width == 0, 'At %d of %d.' % (len(text) % width, width) + + slices = [bytearray(text[i:i+width]) for i in range(0, len(text), width)] + nextSlice = slices.pop(0) + while len(out) < len(text): + if nextSlice: + out.append(nextSlice.pop(0)) + slices.append(nextSlice) + nextSlice = slices.pop(0) + + return bytes(out) + +def unrotate(text): + out = bytearray() + assert len(text) % width == 0 + + slices = [] + for i in range(len(text) // width): + slices.append([]) + + inText = bytearray(text) + while inText: + slice = slices.pop(0) + slice.append(inText.pop(0)) + slices.append(slice) + + for slice in slices: + out.extend(slice) + + return bytes(out) + +print(rotate(alice)) + +crypto.mkIndex(rotate, unrotate, alice, bob, crypto.groups) diff --git a/puzzles/crypto/190/index.mdwn b/puzzles/crypto/190/index.mdwn new file mode 100644 index 0000000..f77e708 --- /dev/null +++ b/puzzles/crypto/190/index.mdwn @@ -0,0 +1,2 @@ +
Alice
e_mse o_rtt pii'i n_dru ueu._
ieron niosn i,ot_ nuvi_ toowd
_idcg o__st _nhae legoh lnfdh
rceir tiasn d_koo efe_s ii_to
__dp_ hroo_ tnyw_ _rt_t +
Bob
'slu_ cnnmo eeq_b gutnn tptn_
st_s_ sodsp ioyr; __r_r fmssl
oiss. aiimn abato ebify t_nso
i_til wamio asnte ensfn necoh
on_tt _shtc na_ol ssloi talrf
_io__ hti.m nsioo i_uor ni)is
_n_u_ c_rgy utto_ o__ia ftase
tt_ro h_c__ hmton _ehec nasta
__rt( rooai ha'fo il_tp yao_e
dacai imnpb _iaft tsiye toa_a
fscu_ i_bu_ ghea_ tpisf thnii
rpfpa _wtyg i_utt _ro_i _tcna
diff --git a/puzzles/crypto/190rotate.py b/puzzles/crypto/190rotate.py new file mode 100644 index 0000000..8d9da3d --- /dev/null +++ b/puzzles/crypto/190rotate.py @@ -0,0 +1,42 @@ +import crypto + +import itertools + +width = 5 + +alice = b'''If we did the morris code encoding prior to this transposition, I don't think anyone would ever figure out the solution.''' +bob = b'''That's basically true of the combination of many of these techniques. Combining a substitution along with a permutation (or transposition) satisfies the Shannon's diffusion principle of cryptography; you want to try to get rid of as much statistical information as possible. statistical information''' +alice = alice.lower().replace(b' ', b'_') +bob = bob.lower().replace(b' ', b'_') + +key = [4,2,3,1,0] + +def rotate(text): + out = bytearray() + assert len(text) % width == 0, 'At %d of %d.' % (len(text) % width, width) + + slices = [bytearray(text[i:i+width]) for i in range(0, len(text), width)] + for i in range(width): + for slice in slices: + out.append(slice[key[i]]) + + return bytes(out) + +def unrotate(text): + out = bytearray() + assert len(text) % width == 0 + + # Make column slices, and rearrange them according to the key. + size = len(text) // width + tSlices = [bytearray(text[i*size:i*size+size]) for i in range(width)] + slices = [None] * width + for i in range(width): + slices[key[i]] = tSlices[i] + + while len(out) < len(text): + for i in range(5): + out.append(slices[i].pop(0)) + + return bytes(out) + +crypto.mkIndex(rotate, unrotate, alice, bob, crypto.groups) diff --git a/puzzles/crypto/200/key b/puzzles/crypto/200/key new file mode 100644 index 0000000..dba2023 --- /dev/null +++ b/puzzles/crypto/200/key @@ -0,0 +1 @@ +the squirrels crow at noon diff --git a/puzzles/crypto/200cbc.py b/puzzles/crypto/200cbc.py new file mode 100644 index 0000000..567a6e9 --- /dev/null +++ b/puzzles/crypto/200cbc.py @@ -0,0 +1,16 @@ + +import cbc, crypto + +alice = b"""Do you think they've figured out that this was encrypted using cipher block chaining with a cipher of C(key, text) = text? If they somehow stumbled across the solution with knowing what it was, the next three will be hard. """ +bob = b"""Well, either way, we might as well let them know that the next three puzzles all uses CBC, but with progressively more difficult cipher functions. the squirrels crow at noon """ + +def C(text, key): + return text + +IV = b'ahiru' +key = None + +encode = lambda t : cbc.cipherBlockChainingE(key, IV, C, t) +decode = lambda t : cbc.cipherBlockChainingD(key, IV, C, t) + +crypto.mkIndex(encode, decode, alice, bob, crypto.hexFormat) diff --git a/puzzles/crypto/210/index.mdwn b/puzzles/crypto/210/index.mdwn new file mode 100644 index 0000000..be87bbf --- /dev/null +++ b/puzzles/crypto/210/index.mdwn @@ -0,0 +1,2 @@ +
Alice
4b 20 61 3d 74 49 36 2d 26 6e 43 2a 61 3e 7d 53 65 68 30 7c 49 26 24 3d 7b
5e 69 3f 37 29 59 6f 37 36 7c 4e 20 3c 30 78 48 6f 24 37 6f 46 20 3c 30 78
5 24 35 26 26 46 2f 30 2d 74 52 25 63 62 4f 16 27 2f 6c 59 11 21 2d 64 b
1 21 2c 6e 59 16 2b 38 75 b 1 2b 74 7d 10 14 21 38 66 a 12 23 74 7a 17
1c 29 6c 7d c 11 21 20 66 11 52 39 23 7b 8 11 21 26 60 12 48 6e 9 60 d
46 64 b 6c 16 4b 6c 47 6b d 5b 23 4f 61 c 5b 29 4d 7a 5e 57 20 1 61 44
51 6f 9 6f 44 59 20 16 74 57 53 3d 9 37 5 71 3f 8 31 57 7a 35 5 2c 41
39 3b 49 37 5b 2f 30 5 3a 4c 20 30 1e 75 56 2a 32 5c 3a 6c 2c 7d 55 23 7b
21 32 4d 24 66 37 3a 49 3f 34 35 75 56 33 34 33 7b 57 7c 2e 31 70 1b 71 39
37 71 57 6d 3e 30 7a 5e 6c 20 2a 35 51 6b 3d 22 3f 59 24 20 27 36 19 6b 33
2a 3d 55 76 24 2a 33 55 75 33 2d 7c 57 7f 33 38 7c 4e 63 2d 22 33 56 64 3a
61 3f 56 6a 25 6f 29 1a 73 36 7c 29 3 6e 64 68 2e 6 62 7e 2b 29 b 69 2c
2a 34 12 75 36 2c 3f 5e 78 3d 6f 38 5b 7a 6f 65 39 17 61 75 67 22 5b 68 75
6d 2a 5f 73 61 7b 29 13 6e 7c 77 2b 5f 60 6c 7b 32 56 21 3e 4f 35 5b 3a 6c
5c 28 52 26 7b 51 24 5b 69 61 53 2f 17 6e 7a 43 60 18 73 71 0 6e 1a 78 23
6 6f 2 65 28 45 73 1a 6b 28 52 79 12 24 2f 41 29 5e 3 3c 4e 32 57 8 6e
4f 24 1b 14 73 41 2e 57 d 60 45 34 5e 42 74 43 3a 40 1 26 48 30 c 6 31
4a 2d 4 49 30 5d 2b 4 4a 62 58 31 1a 51 78 5e 2c 56 4d 65 48 2d 5e 51 37
49 27 5e 51 32 4 68 7b 50 24 12 65 7e 4b 37 13 66 6b 4 31 18 6c 75 e 63
c 62 6a 41 70 4f 7e 69 5c 76 c 7e 63 13 6c a 70 79 5 3e d 6d 74 d 2b
7 6c 7f 4e 79 5 6d 77 1 6a 46 6f 74 1d 6c 5 64 7d 6 7b 15 7f 70 b 65
f 30 6f 10 7e f 34 7a 5f 62 3 32 65 55 30 1 2e 29 55 24 42 32 2a 57 33
1 3b 2f 5d 2f 6 3d 30 5a 7d 4 3c 38 15 7a 9 30 38 1f 69 4 7f 27 0 7e
4 79 2e 1c 2c 8 70 62 0 2b 8 6b 67 0 37 45 24 5c 6 31 4e 6b 51 7 63
4c 77 4e 7 72 46 79 56 1 76 40 36 49 b 6a 50 3c 5 3 77 52 37 c 8 25
45 37 40 1 32 50 3d 5e 7 33 5b 72 5a d 28 5f 75 42 11 76 1c 72 4b 5e 70
17 72 52 56 6a 0 3d 4b 57 79 0 31 48 4d 65 17 3f 46 4e 6e 54 3f 4c 1 6b
5f 31 54 4e 71 59 7e 50 40 67 1a 62 59 4a 7b 59 78 45 56 7d 5b 7e 5b 4a 21
18 56 58 4a 37 5b 7e 5b 41 64 18 46 5f 4f 62 5b 4c 5f 44 62 51 57 50 43 30
56 4a 59 4d 2f 18 52 5a 50 31 1f 1d 41 5e 30 5c 6 45 58 31 1f 0 47 43 2c
5c 18 43 45 3d 57 57 47 4f 6f 5c 59 4f 0 7f 53 43 4d b 68 42 49 45 5b 3a
69 43 9 50 29 78 49 1 1f 36 74 50 8 50 2a 72 56 10 57 3d 63 19 1e 59 2c
6b 1 13 44 3a 28 0 10 59 68 2d 0 e 41 7b 3c b 4e e 6b 2a 10 2 12 6d
26 10 a 5d 6b 2d 1a 14 57 39 3a 7 1d 55 29 35 1 1f 5d 7b 37 1a 53 46 61
31 55 5d 45 72 31 51 11 49 75 20 48 18 6 68 25 7 0 1 7f 66 a 3 16 68
61 48 6 17 3a 71 53 b 11 3a 71 5d 14 1b 66 32 77 e 11 66 28 38 16 c 7d
2d 3b 1f 43 60 28 74 7 44 77 6b 68 8 4e 6b 6d 27 6 54 6b 60 2d e 1b 70
77 31 7 18 64 34 37 5 3 79 77 30 0 1f 2b 76 2d d 19 37 3b 62 35 1e 20
78 7e 36 4 3c 7f 62 76 4b 3a 74 68 3a 57 2d 79 74 33 18 30 7c 3b 3b 5 27
7e 30 77 f 2d 6d 3a 78 14 3e 60 36 6d 57 6c 77 31 64 18 7a 75 2c 63 19 6d
65 30 23 56 6b 6e 3a 6f 4a 6d 68 30 73 4b 7a 78 2c 3f 4b 6e 3b 37 3b 41 3c
36 39 25 5c 21 22 76 3a 47 36 31 6a 7b 49 2a 36 25 7a 43 2a 36 23 70 59 34
75 24 79 57 30 73 25 34 18 6c 30 64 78 59 3e 27 63 71 16 2a 25 65 73 d 78
24 7f 6b 42 7f 29 7d 6e 5e 79 2b 79 63 53 67 2d 36 63 49 78 27 37 60 55 63
30 21 2c 55 77 73 2f 2c 56 25 64 28 25 19 20 68 28 2d 1 3d 79 2c 61 7 21
3a 30 64 f 3b 2d 64 28 13 3d 2b 7b 37 50 6f 3b 7d 3f 5a 6e 74 32 36 4d 6c
78 2e 3f 46 3e 77 20 27 41 3f 38 6f 2a 40 29 7b 62 23 4e 36 6b 2d 2e 4d 2d
63 27 63 2 7f +
Bob
4c 68 29 3d 66 41 63 65 26 7c 47 75 2e 25 62 4 72 23 3c 75 47 69 20 73 61
4d 61 39 6e 76 e 61 20 75 24 19 66 29 3a 3d 1f 70 65 33 20 e 3f 7d 34 37
4d 3e 74 23 31 e 3e 76 29 63 19 3e 75 68 31 5a 19 7c 75 26 1e 5 30 69 3b
10 f 7c 4a 26 5 5 73 57 35 0 1e 25 18 65 2d 1e 3d 1f 7e 20 56 71 5e 2c
6d 19 33 11 30 61 2 37 17 2c 25 4d 75 58 70 66 c 39 43 6a 60 43 36 43 74
6c 59 28 c 28 2f 18 64 4d 7a 25 3 28 40 7d 34 2 37 f 21 77 43 7b 4e 73
77 43 7b 45 21 75 42 30 a 24 73 59 7c 4b 76 3e 16 3e 4 66 28 d 72 2 60
6b 0 6b 1f 7c 7b 4f 29 50 20 38 e 65 56 26 7b d 60 4f 31 7c 42 65 4e 63
6b 45 6c 1 66 6d 46 6c 4e 3a 2e 7 20 f 68 4 48 3f 5 7f 9 7 3a 1e 2d
44 48 78 51 71 7 46 34 55 6a a 4d 78 55 3f 49 51 79 55 26 4f 1e 3b 1a 7a
c 5f 77 1f 6d 1c 44 3b 1c 76 14 4e 77 7 6c 12 1 7d 4 71 6 b 63 18 23
9 5 7c 3 71 19 1a 62 5 6d 1d 55 20 4a 31 5e 14 6c 51 2b 58 5b 77 5b 35
57 14 68 5c 28 5a 1e 24 52 2e 19 1f 21 5a 34 e 50 63 15 68 4d 11 2f 2e 72
4f 1a 63 20 6e b 55 42 2a 6e 6 53 4b 65 7d b 1b 7 50 6a 6 15 18 1f 36
45 54 54 5e 64 43 4d 5d 43 6f 54 4a 58 42 7a 17 44 58 44 7e 11 b 1a b 22
52 4a 56 17 25 52 4e 53 16 70 11 55 57 1c 22 1e 53 5d 16 70 12 49 45 59 6d
17 6 4c 40 7a 6 10 54 47 61 b 18 18 6 33 46 57 5a 49 28 4b 18 42 4e 3b
5c 57 5d 55 26 51 5d 11 14 74 1c 12 53 5b 6f b 5d 52 41 6e 1c 12 5f 9 3c
1c 12 5e 3 6e 16 13 12 18 74 14 8 5e 4 72 18 9 57 4b 2e 5b 48 1b a 7c
48 4e d 0 60 4e 45 41 1b 7a 48 a 5a 1c 67 47 0 16 3 79 45 c 1f 4c 25
6 4d 53 d 77 1 57 51 45 71 42 53 53 45 74 1 4b 57 4b 72 42 4d 4f 4 77
40 4c 57 18 25 d 3 15 57 79 4e 18 11 59 7f d 5 12 43 63 a 4a a 44 78
7 42 46 5f 62 1 40 a 5d 75 c f 0 40 68 2 40 18 47 7f 41 4c 1b 44 61
47 44 12 b 77 51 4c 5e b 70 46 46 5c 44 76 4d 4c 10 58 70 41 4d 19 17 2c
2 c 55 56 7e 15 b 5c 40 2c 5 9 51 5c 36 3 2 1d 5a 30 40 43 51 1b 62
d c 54 0 30 19 2 4b 4f 36 12 c 53 0 37 10 e 5a 4f 26 1c d 59 55 26
5f 4c 15 14 74 12 3 13 1e 75 5 4c b 19 62 46 50 6 1b 75 9 1f 6 1d 6c
f 50 1e 1a 7b 4c 59 1e 1a 7e 4a 44 1 55 6d 47 c 4d 4a 73 45 d 55 56 21
8 42 17 19 7d 4b 40 e 5 7b 8 4e 45 4a 6b e 4f 9 48 76 1f 45 45 48 62
5c d 4c 4a 30 11 42 e 5 6c 52 5e 7 f 7a 42 11 45 40 26 1 50 9 5c 31
7 5b 16 13 6d 44 1a 5a 52 3f 53 1d 53 44 6d 57 0 50 5c 7a 50 4f 12 13 26
13 e 5e 35 74 3 4 57 34 26 9 1f 1b 2f 3c f 50 11 35 3d 18 1f 9 33 22
1e 50 11 34 39 e 1f a 3e 2e 6 50 48 71 72 45 11 4 73 75 55 a 48 7d 20
16 2 4b 66 72 6 19 55 66 6e 2 56 56 67 3c 3b 5c 54 69 3d 78 1d 18 28 6f
35 52 1c 22 3d 21 5c 3 6d 2e 62 51 6 65 7c 63 51 13 26 2e 66 4b 13 25 7c
6a 3 5f 26 67 6f 9 13 67 35 22 46 51 28 2e 35 9 5f 22 3d 22 15 13 29 20
36 14 5f 3f 3d 20 9 13 3d 26 2d 2 5f 33 3a 69 4d 47 34 2d 64 2 4c 32 2b
74 4d 59 38 79 39 2 1b 77 25 7a f 2 6a 39 6a 40 17 60 6b 7c 5f 5b 21 39
31 10 19 6e 22 3c 5f 1 69 35 7f 47 8 6a 2b 3c 5f 5 71 3c 2d 10 47 3e 60
6e 51 b 28 7d 78 1e 10 26 7c 3b 3 15 2e 66 2c 4c 18 23 7b 3a 57 54 38 61
38 4c 18 79 33 75 3 5a 36 24 60 5 5a 79 21 62 1e 53 64 73 2f 51 11 2b 2f
6c 64 18 2a 3c 7c 2b 1a 20 38 7a 36 56 2c 25 74 3c 1a 21 36 74 38 56 28 36
78 3a 1a 33 2c 7e 75 1 39 32 71 3a 43 76 6e 32 7b f 7a 7d 3f 33 17 35 68
35 28 5b 3b 6d 37 3e 17 7a 3f 7a 71 55 35 29 6b 7f 4e 29 7b 71 75 2 68 29
3c 3a 40 27 22 3a 75 47 26 3f 2e 3a 58 3c 20 20 72 55 27 75 30 3d 5a 27 6a
3a 3c 11 64 38 3b 26 9 2b 6d 2c 28 c 2a 38 3b 67 e 2a 6a 2d 7b 7 65 36
6e 3a 4b 24 64 44 75 54 2e 73 49 3a 51 35 21 5e 3c 50 3f 73 5c 3d 1b 70 60
58 3b 19 3f 61 5e 3a 16 24 33 67 30 14 2a 32 24 28 19 36 60 33 28 1a 32 32
7e 67 58 7d 6e 3d 7f 5c 73 6e 79 63 10 52 7d 78 6e 5 11 2f 5a 6c 4 17 62
19 2d 48 56 30 54 62 49 40 62 5f 68 44 4b 37 4f 27 46 4b 65 4b 27 45 40 37
6 68 7 f 6b 45 63 1e e 3e 52 2c 19 f 23 46 63 1d f 26 5 60 1e e 33
46 7c 17 f 22 51 33 32 40 36 57 38 7e 47 21 46 77 3c 8 7d 5 36 70 e 7b
41 35 70 41 6e 4b 2e 3c 46 79 5a 61 35 4f 2b 4e 6b 79 41 30 43 23 61 e 29
45 29 7f 7 2e 4a 66 3d 48 72 9 27 71 4d 65 19 3c 3d 43 37 19 3c 3d 43 30
8 73 7f c 6c 4b 32 33 b 7b 5a 7d 39 5 6a 5c 32 3c 19 38 58 38 24 2 23
55 70 68 19 3e 16 77 61 0 6c 1 70 6c 1b 3e 1 70 6c 1b 39 10 3f 73 1b 26
16 24 76 19 31 6 6b 6e 19 34 4 76 66 5 66 9 70 6d 2 60 4a 31 21 43 32
7 7e 2c 42 67 44 78 34 d 77 52 65 36 11 25 50 64 7d 5e 24 46 68 7a 42 76
b 27 38 d 2a 48 21 20 42 3b 44 23 29 d 2f 55 23 28 42 2e 59 21 21 d 2c
56 2f 2e 7 7e 42 28 23 1a 2c 55 2f 26 1b 39 45 60 2b 1d 25 1 7b 67 13 24
42 60 63 19 2f 1 66 7c 56 35 7 7b 75 19 69 44 3a 39 58 3b 48 3b 30 17 26
c 74 28 10 31 2 3b 34 d 2c 7 31 2b 11 31 16 2d 67 d 22 1c 26 2b 11 3f
5f 67 67 50 6d 12 28 63 5a 3f 6 26 7c 15 3f c 2e 78 e 6d 41 61 3a 41 31
2 62 39 41 28 41 62 20 5a 76 2 4c 21 58 6d 4d 3 24 43 38 42 0 68 48 25
1 1c 71 53 3f b 1d 3a 1c 20 7 0 33 53 7c 44 41 7f 12 2e 54 5b 70 16 2f
17 40 74 1c 7d 18 46 7e 16 2f 14 5c 66 57 7d 59 13 24 18 21 18 5c 1c 1f 36
5b 70 1f 1c 2b 4d 6d 53 3c 2c 5a 22 50 35 7e 6a 3d 5d 39 69
diff --git a/puzzles/crypto/210/key b/puzzles/crypto/210/key new file mode 100644 index 0000000..73d7fe3 --- /dev/null +++ b/puzzles/crypto/210/key @@ -0,0 +1 @@ +The Colour Out of Space diff --git a/puzzles/crypto/210cbc.py b/puzzles/crypto/210cbc.py new file mode 100644 index 0000000..c554878 --- /dev/null +++ b/puzzles/crypto/210cbc.py @@ -0,0 +1,20 @@ + +import cbc, crypto + +alice = b"""I'd say this was easy, but we didn't give them the key, did we? I'm adding some text to give them something to work with: Commencing his descent of the dark stairs, Ammi heard a thud below him. He even thought a scream had been suddenly choked off, and recalled nervously the clammy vapour which had brushed by him in that frightful room above. What presence had his cry and entry started up? Halted by some vague fear, he heard still further sounds below. Indubitably there was a sort of heavy dragging, and a most detestably sticky noise as of some fiendish and unclean species of suction. With an associative sense goaded to feverish heights, he thought unaccountably of what he had seen upstairs. Good God! What eldritch dream-world was this into which he had blundered? He dared move neither backward nor forward, but stood there trembling at the black curve of the boxed-in staircase. Every trifle of the scene burned itself into his brain. The sounds, the sense of dread expectancy, the darkness, the steepness of the narrow steps-and merciful heaven! . . . the faint but unmistakable luminosity of all the woodwork in sight; steps, sides, exposed laths, and beams alike! """ +bob = b"""No, and they\'ll have to figure out the key for the next one too. Here\'s some Lovecraft: "Nothin\' . . . nothin\' . . . the colour . . . it burns . . . cold an\' wet . . . but it burns . . . it lived in the well . . . I seen it . . . a kind o\' smoke . . . jest like the flowers last spring . . . the well shone at night . . . Thad an\' Mernie an\' Zenas . . . everything alive . . . suckin\' the life out of everything . . . in that stone . . . it must a\' come in that stone . . . pizened the whole place . . . dun\'t know what it wants . . . that round thing them men from the college dug outen the stone . . . they smashed it . . . it was that same colour . . . jest the same, like the flowers an\' plants . . . must a\' ben more of \'em . . . seeds . . . seeds . . . they growed . . . I seen it the fust time this week . . . must a\' got strong on Zenas . . . he was a big boy, full o\' life . . . it beats down your mind an\' then gits ye . . . burns ye up . . . in the well water . . . you was right about that . . . evil water . . . Zenas never come back from the well . . . can\'t git away . . . draws ye . . . ye know summ\'at\'s comin\', but \'tain\'t no use . . . I seen it time an\' agin senct Zenas was took . . . whar\'s Nabby, Ammi? . . . my head\'s no good . . . dun\'t know how long senct I fed her . . . it\'ll git her ef we ain\'t keerful . . . jest a colour . . . her face is gettin\' to hev that colour sometimes towards night . . . an\' it burns an\' sucks . . . it come from some place whar things ain\'t as they is here . . . one o\' them professors said so . . . he was right . . . look out, Ammi, it\'ll do suthin\' more . . . sucks the life out. . . ." The Colour Out of Space""" + +def C(text, key): + out = bytearray() + for i in range(len(text)): + out.append( text[i] ^ key[i] ) + + return bytes(out) + +IV = b'ahiru' +key = b'color' + +encode = lambda t : cbc.cipherBlockChainingE(key, IV, C, t) +decode = lambda t : cbc.cipherBlockChainingD(key, IV, C, t) + +crypto.mkIndex(encode, decode, alice, bob, crypto.hexFormat) diff --git a/puzzles/crypto/220/index.mdwn b/puzzles/crypto/220/index.mdwn new file mode 100644 index 0000000..23afa0b --- /dev/null +++ b/puzzles/crypto/220/index.mdwn @@ -0,0 +1,2 @@ +
Alice
1c 1e 38 7 52 4f 72 72 71 2b 18 58 6 52 4 74 65 6c 78 37 16 53 18 c 1d
6c 6e 36 3a 2b 5f 4f 4c 5 54 2a 27 7f 20 25 18 43 47 52 0 67 73 77 31 27
18 45 47 7 11 33 7d 7d 65 66 13 46 58 14 2 37 76 72 24 61 1d 41 17 1e 53
64 73 78 20 67 58 e b 1 44 62 31 3e 68 62 1e a e 45 1c 6b 6c 7b 79 65
1 0 1e 16 15 7f 70 72 20 64 17 13 5f 7 0 2d 20 72 34 62 19 c 40 41 5d
34 38 7e 2c 29 c 5d 59 18 4d 2b 39 64 38 38 a 4b 43 58 18 35 76 65 27 31
11 54 52 56 4f 72 18 7c 7a 76 a 56 17 3f 1f 7e 7b 7e 24 5a 1b 36 5e 9 45
7e 24 77 4f 61 57 e c 40 3b 6d 5e 77 63 2b 1f 46 4d 2a 6 22 72 3f 35 a
4b 73 4a 13 15 6a 7a 24 6 77 50 1f 4 5d 26 61 52 31 69 7d 57 11 e 72 6
79 75 3b 7e 52 5e 21 16 18 5e 75 38 2e 44 71 e 19 1c 5b 30 7d 10 7c 7c 3f
5c 50 12 76 10 71 64 3b 39 56 5c 22 1e 44 5c 71 28 7c 56 64 5c b 19 4d 35
7c 41 2e 79 2e 40 59 5c 20 a 67 73 25 2b 0 5 75 8 6 41 28 20 76 1 6e
56 1 5e 45 75 2b f 76 71 3f 56 4a 47 6a 1e 33 7b 22 6a 2 47 6a 13 16 9
7a 64 26 4 65 48 a 5a d 72 3f 5c 24 7c 69 +
Bob
c 1b 35 0 52 41 31 69 63 20 19 4f 33 48 17 46 74 39 29 26 56 6 32 1d 47
12 28 3f 75 6e 4b 7 7f 4d 1d 5f 74 25 60 3e 49 12 32 4 5 47 25 69 70 70
1e 50 29 40 5e 4c 3b 3e 18 32 56 44 60 1b 79 13 14 33 64 74 5e 6 76 34 b
3a 7d 3b 26 5b 49 3d 5f 1e 47 3e 26 3d 1d 79 7 38 57 48 3d 3e 4e 6a 55 68
1d c 56 2b 30 3e 5d 3d 78 4e 55 6e 1e 29 1d 7d 3d 37 f 42 58 62 19 52 7d
7d 9 39 c 72 19 13 15 6c 7c 35 c 6d 7b 19 5 76 15 78 9 7d 7d 70 11 58
50 31 15 18 77 71 57 35 5d 6b 15 3 5 38 29 71 47 70 23 5d 13 38 5c 26 51
3d 25 33 48 55 56 75 48 57 66 2d 46 2 1d 2e 6e 4b 5a 27 76 3b 12 a 6b 49
7e 24 1b 61 1e 7e 3e 1c 48 5 3c 6b 1f 4d 21 3f 53 1c a 29 71 9 5a 32 26
3e 42 10 67 12 30 7d 57 26 9 33 7b 44 5d 47 28 28 56 5b 32 34 51 43 8 3a
37 56 5f 71 61 2b 4 5b 76 19 2c 6b 52 24 13 34 61 49 4b 45 3e 3c 14 0 2a
7a 5e 1e 53 20 3e 49 12 3b 3b 32 49 59 21 5c 37 72 5d 3c 45 35 3c 17 26 59
72 3c 15 4b 54 79 30 52 5b 2a 34 5e 59 5f 7b 79 1a 5c 3b 2f 70 49 d 72 1b
2d 73 1f 3b 6 3f 69 48 a 58 2c 36 4a 5 2a 6a 4b 43 42 6d 63 2 1c 2e 25
3c 4d d 67 5a 6b 35 59 6d 15 79 74 5 41 1a 25 63 0 58 23 68 57 5 17 39
60 55 48 20 7b 1 5b 4e 75 54 3d 36 76 3a 55 56 3b 5c 52 5f 3b 7f 39 4e 3a
4a 53 4b 1e 3d 6b 55 24 34 6a 43 5 e 75 58 60 78 34 6c 12 46 32 2 19 2
66 71 27 5c 39 43 15 e 14 2f 60 f 63 74 70 c 15 14 67 7 7c 69 2c 66 e
4b 61 15 7 46 7d 34 39 2 66 4d 3 19 14 70 39 19 28 70 63 8 b 4d 71 4
3f 63 6d 62 51 1f 34 51 2 e 71 67 7b 18 64 1f 17 14 9 71 77 1f 77 37 66
2 15 3 70 45 61 31 39 35 5 51 6b 41 45 54 29 35 3a 4b 20 54 54 5f 50 6b
7f 19 3c 31 32 5f 57 1e 77 59 7a 3d 2c 77 1e 44 77 5a 49 12 2d 60 36 57 26
16 51 5e 14 23 2a 4d 7f 22 7d 17 1a 4d 6d 4b 38 65 7f 3a 1d 17 3d 18 31 5f
77 7f 7a 52 5f 14 7f 0 1a 26 64 54 61 11 7f e 1a 44 27 7c 21 5 2e 6c 55
42 36 1 67 d 62 62 29 16 b 5a 6a 17 6 36 63 53 29 4a 6e 4a 1 1a 73 38
7f 18 39 72 17 5c 65 b 70 52 7f 31 29 16 19 5b 7d 5f 53 7f 7f 9 3c 18 3c
1c 54 1a 7b 5b 77 35 7d 24 1a 5d 68 50 46 66 3b 46 32 7 6a 56 3 5a 28 27
2d 54 22 23 49 4e 27 d 36 4a 6a 25 2a 7 44 44 64 1a 4c 60 75 40 22 16 21
47 44 1 28 64 6e 10 67 30 8 47 78 6 75 5f 26 3a 22 16 18 4d 6b 47 5e 61
28 4 63 4b 2e e 4f 46 24 2e 31 e 6a 20 57
diff --git a/puzzles/crypto/220/key b/puzzles/crypto/220/key new file mode 100644 index 0000000..de2bb17 --- /dev/null +++ b/puzzles/crypto/220/key @@ -0,0 +1 @@ +open meadows diff --git a/puzzles/crypto/220cbc.py b/puzzles/crypto/220cbc.py new file mode 100644 index 0000000..acf0e0b --- /dev/null +++ b/puzzles/crypto/220cbc.py @@ -0,0 +1,15 @@ + +import cbc, crypto +from transform import transform + +alice = b"""You know, I just realized it's kind of smug for us to be talking about how easy or difficult these puzzles are we we're making them rather than solving them. We've tried really hard to make them so that you don't have to follow some specific thread of logic to get to the correct answer; you just have to puzzle out the mechanism involved.""" +bob = b"""The next crypto function is something simple, but new. Here, have some more Lovecraft again: Ammi shewed them the back door and the path up through the fields to the ten-acre pasture. They walked and stumbled as in a dream, and did not dare look back till they were far away on the high ground. They were glad of the path, for they could not have gone the front way, by that well. It was bad enough passing the glowing barn and sheds, and those shining orchard trees with their gnarled, fiendish contours; but thank heaven the branches did their worst twisting high up. The moon went under some very black clouds as they crossed the rustic bridge over Chapman's Brook, and it was blind groping from there to the open meadows. open meadows """ + +IV = b'ahiru' +keyE = [2, 4, 0, 1, 3] +keyD = [2, 3, 0, 4, 1] + +encode = lambda t : cbc.cipherBlockChainingE(keyE, IV, transform, t) +decode = lambda t : cbc.cipherBlockChainingD(keyD, IV, transform, t) + +crypto.mkIndex(encode, decode, alice, bob, crypto.hexFormat) diff --git a/puzzles/crypto/230/index.mdwn b/puzzles/crypto/230/index.mdwn new file mode 100644 index 0000000..3ecd630 --- /dev/null +++ b/puzzles/crypto/230/index.mdwn @@ -0,0 +1,2 @@ +
Alice
d3 d 1f 7 df 59 a2 c1 aa 1 c3 4b 69 4c a8 6a ab 3 ef 4c a 46 a2 87 b4
a7 e3 5d 69 53 21 80 f7 8 c7 ca 25 5d e8 8f 6b c7 b5 48 71 c1 6f 50 a8 e1
64 f7 e4 88 93 f7 8d 9e 24 3e 39 1a 39 c8 d2 d5 98 dc 28 79 5d 1b 6e d7 b
ef 9e a 39 86 7c 17 a4 a2 6e c1 b5 29 86 11 51 31 b 3b bb d0 4 fa f 9d
2c aa 9d a6 38 f6 84 63 53 a7 71 5a ca b5 3b b7 ee 71 53 d1 79 8f 2 b5 6f
f7 2a e2 5a e 89 c2 89 e8 f6 11 57 29 8d 82 98 d8 c9 67 62 76 59 69 5 c2
f9 d7 cb cc 9c 3f 58 6f 2c 74 c9 d9 e c c2 3 19 ba ab 71 aa aa 94 8d ef
46 4f 74 3 67 d1 e2 13 e5 16 66 38 f5 50 e9 b4 b 42 a7 25 73 dc bd 94 b
b5 51 52 30 f3 7b a2 a0 7 49 b2 76 96 fb b8 62 b6 39 9b 24 c6 1f 1b 38 d4
6f ac be 18 29 bd 7d 90 f8 b 1c f1 25 99 e9 dc 79 c4 31 94 1c a 6d dc 33
dc a7 2 72 de 1c 2b af 4 45 81 b9 4e a6 f3 5d 79 1a 4f 64 83 f8 af ee 0
65 7d 5b 8d b6 cb 9 f4 73 26 66 a7 80 d5 d2 f2 42 31 69 45 79 d1 1f cf f3
f7 54 c1 7c 9e 87 d1 71 dc 37 11 50 1a 7b db 9a 90 b9 e2 7f 5e 24 4d 99 e5
df c1 eb 7f 89 42 62 80 a1 7 d3 f5 23 55 ad 15 54 d3 c2 4f 94 d9 35 60 fe
54 6d df b 26 de 2 6f 81 d6 2 90 0 20 72 ec 50 c3 d3 e1 8b 90 74 76 92
6d 27 1d 4 23 8 7 ab ac c2 aa e7 45 4b 73 8e 81 e0 e9 e1 27 16 95 4b 43
ca 98 33 ad e3 51 1f d4 48 89 97 ab 7c f3 2c 59 3f 15 55 a1 dc ba f7 c3 48
57 7a 9e 73 c5 c2 fe 3d d5 77 6d 8d 1f 7e 0 5 6d b4 19 bc aa 5 53 b9 90
8e ac ff 4e 2c 2d 4d 9d b2 cd cd ad 36 59 68 43 44 d8 c2 c8 d0 d0 6a 65 63
59 58 1e cb 14 d9 98 aa 64 fc 53 17 5f e 52 95 bf fa f2 fe 1c 3b 5a 9c 93
a8 fb ef 3b 20 40 3b 9d d4 0 e5 c9 32 69 bd 4b 5d da c 67 eb d4 7f a4 1d
72 50 d5 84 aa fd de 73 6f 27 7b 57 5 e1 c2 e1 97 b2 92 77 8b 24 92 35 e5
24 ca 39 1b 93 cd 2a d3 b6 20 13 c2 7c 5d cf d5 6d 12 83 45 43 8 f2 32 ec
ee a6 52 c0 8d 71 42 bc 20 55 e2 d0 9c c8 f0 46 58 3f 6c 50 d2 d9 dd cc f3
5c 19 7c 68 99 df 9d dc 1d 33 50 1e 79 c3 c9 d8 9f f 25 47 50 1a f1 ce fd
d8 9f 85 43 27 51 1a 39 ea 9 e6 aa d8 98 b4 86 86 38 3f 5a 26 66 d1 a2 eb
a 5 65 59 98 a6 a0 3 bf 6 4f 32 a6 4f ad c3 f2 53 ea 5b 6d 32 f2 4e fb
e1 bd 9f ee 92 71 3a 41 88 39 f0 c8 a3 20 a4 30 6c 42 d6 43 b4 cc ec 7f f9
74 69 9a 1e 99 b4 4 1 b0 37 23 ab b1 90 d3 cc 39 93 3 72 66 c0 75 be 19
9 52 6 62 b7 aa 92 a7 13 5a 31 5c 89 d6 eb f7 e8 23 75 55 8b 8d d0 7 bf
24 2b 30 b5 59 c7 b c9 54 f7 64 a3 6c ea 8c c4 4c 2 4e 38 6c ed b6 b2 d4
f 80 46 59 69 9d 15 eb ec 1a 43 9d 82 80 be a5 15 25 24 66 3a a1 ca c8 11
a2 4d 75 28 a2 49 d9 d7 dc 4a ec 50 67 3c f4 73 d3 2 d1 5c 9 5c b7 70 f6
93 9c 45 18 9d 55 2e ea bf 3b 9f e 6d 56 de 1a ee 7 f9 7a de 82 a8 9a 13
5d 26 88 38 b2 c2 cd 68 df 24 26 64 6 6c d0 c9 a b2 cc 73 43 ea 46 64 1e
a5 79 ee b ba 4e e 8d b1 9a d8 af 23 23 1 67 8f cc cb e3 5 2a 41 64 91
a4 be e8 d 1 84 4d 89 f3 b7 2b d9 2c 61 56 d 6a c4 4 fa 9f f 76 b3 9c
18 ef 1e 5e 31 d8 86 b2 f6 e4 6d 12 55 84 9e 8 96 b7 26 34 a7 13 5b c6 1a
89 b2 f7 66 fa 11 40 98 d 5a ab e8 3b bf ba 38 7c cb 5a 5b b5 f9 67 ba 85
35 78 17 99 2c c6 f9 ae 7f a1 5a 76 5a a1 4b c1 b6 f2 40 f1 27 45 99 eb 61
9 d6 36 8a c 83 5e a1 24 b5 65 d8 44 c7 59 f 61 a4 7b f0 9c 4 88 dd 61
16 a8 33 3 1f 9f 88 c0 ae b3 14 68 64 47 4c 9e c8 7 e0 f7 19 66 e9 9b 89
9a 8 4b 31 23 1 e8 fd a3 c2 a6 8d 5f 4b 70 4f 20 81 fc 18 ea c3 2b 5c b4
86 66 d fd 4e 25 7 bb 23 c7 b3 a6 4d ca 75 3a 35 fe 2e e7 bb c7 8c c7 9f
7d 6f 2f 65 41 f2 7 c0 17 a3 7d a9 74 b8 8d f 41 13 98 6 a4 d8 ff 78 d1
30 18 88 1f 62 f0 92 3f ac 1e 30 13 e1 51 b3 bc 9c 94 e5 9d 22 29 20 4b 3d
b3 bd 0 ee dd 4e 7d a5 87 73 e7 8 59 69 e4 87 e8 f6 1c 9e 69 8d 99 b2 2a
7 6d 36 40 a 91 8 d9 fa b1 1 9b 39 5e 30 91 1f 1b fc df 16 9e ae 91 7a
da 3f 45 3c 1d 1e cd e0 a1 b2 9f 63 8b 56 59 1d c3 1 c2 f8 af 23 a2 73 9c
40 b6 50 a1 44 e7 38 f4 87 f1 8f bd 85 69 8f 29 3c 33 cf 5 c7 b5 c0 31 ab
6a 46 74 17 57 d ef 1a a5 b9 81 8a b9 4d 58 11 1e 9b e0 f7 a6 aa 7d 95 8d
31 46 13 7b 23 c4 d4 b8 dd c9 6d 63 56 3 47 b 6 e4 aa a9 a6 a8 44 22 43
45 88 f5 c9 f1 ef 64 57 67 9c 31 c f6 14 44 a1 a9 80 b7 fe 87 4b 27 56 90
69 da d2 c1 2c c 1a 60 68 d5 a4 ae c0 1d 44 4e 4d 67 f5 ac +
Bob
d4 5 4 a2 df 5d ae ea 86 1 4 41 61 67 b7 a8 a1 11 6 52 4c 81 a7 b2 c5
c1 5b 89 8a 39 16 ab 7b 7c 14 d8 8b f1 14 b3 18 20 94 b3 9d ac c8 3f 40 47
38 61 d2 eb a9 b7 f2 32 4c 58 46 73 c4 ef ea d1 fd 65 51 66 54 73 3 e5 11
d8 0 e4 4d f3 18 b3 8d e0 9e d8 3e 22 96 5 18 ca c9 32 b3 d8 5e 66 16 59
43 dd b b6 c4 9f 1d ed 27 63 2c a9 4f c8 f c 28 ff 65 a9 93 c5 21 17 43
1a 25 cb 82 f1 9a b6 2d 30 98 2f 33 f c9 33 c5 b7 e5 2f d2 6d 33 47 dd 7a
8 c1 e2 3 e5 9e 64 42 bb 8c 16 8 e9 46 1 da 9f 80 f6 ab 1e 1b 27 95 25
c2 ac c8 3e c3 57 8c 7c da 62 e5 6c 12 7b c2 84 f4 b9 dd 6a 2b 87 9b 78 e4
b2 15 7d 1d 93 4a db df cf 5 d1 69 7a 31 ac 65 c da db 57 f9 a7 3a 7b fe
8d 35 d7 c 26 2e b9 72 a6 df f2 4e 1a 45 77 79 ae ae f3 12 bf 3a 5c 9a b9
4c fa f1 3a 5e dc 7f 53 dc c1 56 b f2 71 7f c1 a8 95 e3 1d 58 84 77 86 bd
dc 64 0 32 58 52 1 e0 ca f0 dd af 40 68 82 56 40 ed 4 29 e6 d3 4f b0 a5
71 50 e4 5c 40 b7 e7 8f e8 e8 38 72 3d 48 97 b2 f7 d1 a8 1 35 81 78 4a ae
fb 22 1b c1 4b 75 b6 fd 7b ed f0 76 9a 19 8c 62 2 7a ba 2a c6 91 11 54 ce
51 16 bb e0 20 97 b9 57 40 dc 1 3c b9 e9 7b 91 c8 57 8c b 57 5b fd 25 85
d6 e8 5f d7 37 55 7a fa 39 de e9 ff 5a a1 41 7d 72 eb 93 a3 fc fc 88 3e 4e
76 88 68 a1 e0 f6 68 1a 52 8d 63 f1 f6 b6 3a e 85 9e 9a cb 81 10 1 32 29
67 9d ae c7 da ce 1e 43 6a 76 6d 9a a5 f 1b ff 2e 44 a8 86 8c e e6 5e 2b
25 aa 91 f0 d4 b 4f 27 9b 6e ab ee cd 39 19 8d 81 7d dc b5 53 2e 13 78 9f
e3 cb a4 d8 5a 73 6c 48 7f fc f9 cc fd 1e 61 8d 69 8b b3 c3 24 7 6d 49 75
c7 e7 3 ec 10 29 88 a7 4c b9 bd 2d 46 f8 9b 37 c8 e2 61 6 c4 13 42 5 bd
50 d3 f6 b3 58 e5 56 56 56 ea 80 96 f9 fe 8e 60 20 98 63 31 f2 b4 78 13 a6
8b 78 d8 a9 56 12 b8 71 40 f2 99 78 12 e8 65 14 f6 b9 97 1e d4 75 4d 26 bd
5a f9 e4 c8 5a 9e 78 8a 6c ba 2e f0 31 cc 4b ca 79 a1 6d e9 51 fd 83 e2 80
db 78 30 46 22 52 f4 d9 f1 d4 db 34 6b 94 6e 56 b8 4 27 e7 de 41 e4 cb 82
50 e4 87 41 2a 90 88 28 fa a 19 29 c7 98 b8 9c c5 29 2e 59 15 63 da 0 dd
c7 b 41 e0 72 6d af a3 8a e7 7 44 85 26 8e 97 e1 67 c7 23 1f 84 c9 6a dc
82 1d 27 ce 47 2c 99 9 68 e8 a2 1c a0 11 8e 49 ae 55 f7 2a ea 8e fd 98 a6
4e 22 94 35 4d 92 b6 3d 1b e3 1d 40 d8 fd 99 9b e5 6a 98 79 5d 63 18 3 19
c2 b f7 c0 b8 6c aa 95 6f 5b 6 39 77 9 fa 90 bc d9 83 88 1f 39 7f 65 23
ad f5 a1 ec d9 4c 79 48 93 79 c1 b9 e0 36 11 64 79 94 c4 b8 7 fd 74 73 54
ed 3d 17 e3 f0 33 c9 81 8a 50 a5 66 2f 31 ed 8b e ce c9 88 6d ad 63 64 37
b3 4c f 0 19 39 e9 81 a7 b5 b3 8c 27 8f 9f 1e 18 9 2a 34 c2 96 a6 a2 1c
60 13 47 45 ab f5 96 a9 ee 57 3b 17 49 99 ed b5 9e e9 2 57 47 16 88 ba b9
e4 97 26 9e 25 87 57 d7 3d c0 2f 97 78 d1 20 c9 12 1b 65 cc 2f 9d ab cb 6c
b2 13 51 69 b 3a df b3 d 86 b2 1f 53 a8 2f 35 aa e0 53 11 be 4c 40 e0 86
35 e9 f4 88 31 bd 8e 54 3c db 3c 1a b4 db 68 fc 93 5b 7e 11 7b 1d fe 1e c1
bd c9 96 bd 23 33 65 39 22 ce f5 c ca 6 60 7e 98 2a a7 11 f7 1c a 47 86
8b dc be f1 2d 6d 55 55 90 ca 4 db f4 24 6d ad 55 58 4 14 45 d1 b8 a0 85
e0 62 98 52 2a 74 c2 3a e0 cd fe 65 1a 96 68 73 f1 bc 25 c b5 75 4e c0 b7
38 6 ae 67 4d f8 a8 49 0 ef 8c 86 ab cf 65 6c 2b 5e 7c 13 cc c9 fd dc a9
64 60 8e 3c 26 c 9 2f d4 e a9 ac ca 64 87 8f 38 62 7 69 2f b7 10 a6 0
c8 39 b7 54 bd 28 b8 40 fb 4c 4 78 e6 94 c6 e4 f7 8a 30 2e 6b 7a 29 df 16
dd f3 cc 73 bf 58 33 64 16 54 98 bd 3 a0 f3 2e 7d b9 51 61 ce bd 4f ff c
65 7d e0 9f f4 4 f3 9b 2d 64 93 7d 3c 13 14 1a bd ce ba b9 9d 31 62 4f 9b
1d c3 f e6 45 af 6b aa 4a fe 3d f9 4d e4 9b b8 39 eb 8b 22 4d f7 4d 4e d1
e0 7e a3 b2 6e 88 f1 50 59 d6 20 74 f3 c3 3e c8 f5 80 25 a4 53 70 2e b 4f
95 fc c9 81 c5 1f 79 2b 30 70 9e fc db 10 1e 17 8e 3d af 85 9a 1c ce 85 6f
15 af 2e 37 9 90 8f c3 df af 2f 6f 6c 77 4a f1 f8 4 18 aa 86 8c af b4 4e
6a 1e 4a 5f f1 5 90 fb b5 53 ae 22 5d 46 c5 47 2 f8 f0 60 db e2 8c 97 14
50 71 6c 38 b2 90 5 4 a3 92 25 91 b6 46 4 c6 14 96 e6 af 41 a7 6f 4a 56
d1 43 d1 e6 fd 17 e7 70 94 98 a4 47 4 3d 78 40 e8 e4 cf 10 ef 7b 44 73 bf
8c fa f3 0 5a 6c 72 9a b5 ba 4 fd 31 4a 21 e4 74 c2 ae d2 8d b4 6c 4d 7d
1 74 18 eb 18 e7 fb a8 92 be 49 7d 43 39 52 d3 e a5 d8 c4 59 ee 53 70 63
9f 7a e3 17 1f 1e f6 91 b6 85 c2 84 3e 59 23 57 64 a1 ed d4 e6 a 47 53 9f
74 a1 f1 f6 32 be 6b 67 62 d9 4c e c9 1b 78 c1 ee 66 bb e5 64 4e 9 9d 99
d ef af 43 3e 9d 8a 5f a5 ca 1b 2f 81 53 6d 99 c3 32 b5 14 19 67 12 47 84
ad 8 a9 e0 34 73 9c 43 8b 1c b 1d f1 29 bb 96 ae 5f f 51 2 4f ed f1 ed
a5 e1 4f 86 9b 39 80 e6 27 3d b3 60 93 d1 d8 47 5 75 6b 70 e6 e5 19 9 13
7b 71 bc b3 fd f5 f4 5f 49 9f 3b 7d ec af 33 b4 a 59 91 d3 47 9e bb 27 7e
e8 2e 59 c6 a6 8d cb f8 7b 4d 2f 2b 8c 14 b7 be b8 2b f4 26 4e 78 ca 9e c2
92 f9 2a 30 69 2b 39 d8 a1 8 b1 f9 71 40 85 77 75 10 ef 23 b9 f6 a7 63 df
30 78 40 c5 7b b7 a a0 77 1c 40 ab 52 15 c4 d4 43 f9 fb 79 5d d6 8f 95 d9
83 82 3f 34 7a 2a 11 e1 dd e4 e a5 8b 78 4c a7 49 31 d8 ed 4f ec 13 7d 94
e7 4c a0 f 7 8d e0 80 b3 a2 53 86 31 4a 54 d9 66 d0 e9 c7 43 f5 64 4f 7d
ed 70 c4 f8 18 88 f4 79 58 b9 12 34 18 b8 55 99 c0 af 4e c5 10 6e 48 b2 72
9f 2 ec 59 0 17 a7 61 ee f4 d9 46 11 8e 5c 6e e9 bc 2d 84 7 8f 9c df 34
a6 22 7c 68 a7 4e ce dc c 48 dd 6c 68 a0 f7 52 ff c8 4c 66 dc 78 6f c2 ce
5f e a 6c 36 e8 a6 b1 6 1e 80 47 5f a0 c6 14 ef fa 80 76 9d 83 96 31 3
12 63 39 c5 ae d6 5 d7 6e 43 56 a0 75 d2 ef df 32 d7 6a 89 5e f2 7b 11 6
82 80 1e a6 ad 10 16 b3 51 21 c2 d6 29 ff cc 6c 16 df 99 6b cc 92 7e 2d 0
2c 25 13 c6 ac a1 5 a4 71 40 33 e5 46 14 a0 b0 70 f2 f4 4c 21 7 9c 54 f7
b1 a3 7c e0 61 34 83 1e 8c 0 c9 17 bd 28 e0 67 92 5c dc 88 c9 25 f7 70 2c
2f b6 9d b c3 b3 33 31 ed 6f 36 c1 13 91 a be 6e a4 77 a8 36 fb 43 d9 21
b9 8f e6 6d d2 7f 18 8e 6 63 fe d8 3f b5 13 78 59 dc 50 fd f1 d0 70 fc 27
7c 59 2 92 c8 f2 99 b6 3e 77 36 2a 5f c9 d9 b9 c4 fd 2f 75 30 54 8f c5 10
f0 dd 23 2b ac 78 58 5 d8 5b fa 96 e7 9f ff 62 56 95 39 9a c6 e5 30 de 28
65 71 c5 2 c9 5 b1 73 ae 74 eb 42 9 5c 1f 8b e5 ac bc 85 2e 7c 21 9c 32
c9 f0 cf 7c d0 6c 30 60 16 7f cc c7 b a6 19 2c 53 a5 45 fb c8 d6 40 fb 25
5d 16 f5 62 ce d1 93 83 17 3f 17 1c 2f b6 e1 a0 95 dd 22 43 34 1d 78 6 f5
a3 99 1c a5 65 85 15 b5 59 0 6b 9a 55 bf b1 9 1e b7 50 52 ac 9f 5b bc b2
33 11 c1 9c 5d a1 9d 23 28 ff 44 5d ce d7 94 d0 d8 7c 72 7c 41 5e dc 6 16
e8 d3 7f ae fe 73 52 f 5e 9e a3 92 7 f 3e 4a 1c e9 ca e6 c3 99 89 42 4e
6e 17 3f f3 b6 fe 9f da 55 26 79 13 76 ea e f8 9d 12 4e b8 7f 11 b9 c3 98
d 9e 9b 70 37 81 28 3e f 19 29 c7 cc f1 b7 c7 62 2c 61 52 50 c2 de 4 e3
db 50 68 ad 8a 6f 90 1c 4b 31 fa 50 a8 e4 a7 62 90 47 87 89 fa 18 e2 69 3b
7a 99 83 c e5 f3 18 30 ec 80 8b 91 dc 46 60 21 19 7b c1 c0 bb 96 19 6b 20
3a 13 fb e1 c8 b5 a0 5d 88 6e 7b 21 f1 25 e fe cc 5f b b1 70 68 81 af 5c
f5 9 34 5c fb 3b 9d dc c3 91 fd 14 71 25 30 23 a2 14 d9 cb 5 4d b8 43 35
97 e2 51 f3 1f 17 77 ff 61 c1 98 f1 21 1a 68 2c 72 cb fe c cf f5 6e 90 a8
5c 35 e9 70 24 d4 ba 9c 19 d4 60 7a 28 b7 73 c0 ff de 22 e1 20 72 6d d7 9f
0 f0 8 39 34 91 7d af d3 a4 18 f3 48 35 5d 90 7a ed a3 87 19 8 8c 53 2d
98 a5 1 b5 cf 1f 41 e3 46 69 9a d2 95 e5 e5 1 56 77 97 8a e7 d5 e1 2 2e
85 67 84 ac 16 17 f 36 45 ad 90 a7 16 ea 48 2 33 a5 98 e3 a9 c0 8d 78 79
47 65 6f 1a 18 ee 0 d1 fe ba 86 a1 62 8e 21 26 81 a 2d c0 b6 29 ea 13 7d
35 cc 89 ba 1b fb 58 26 9e be 62 98 d0 3d 54 fd 58 41 df f0 23 d9 f1 7d 50
b9 51 94 1c ec 4d d8 39 83 80 a7 18 d8 33 2c 89 b1 38 c4 cd 29 35 df 7a 68
b3 f5 79 1b e1 75 7c f fd 8b f4 f8 f1 62 2c 7f 7d 53 d c2 d f2 b5 bf 6a
a8 32 55 50 e 45 be fa e6 b5 ab 31 9c 4a 5c 8d a1 2b f8 ff 1f 40 c7 58 91
9e ef 79 f4 3c 50 4f ef 9c db c2 af 66 7c 68 65 38 c6 8 0 4 cc 26 ac a3
9a 67 6 5d 4e 2f f3 b3 fc c4 cd 33 49 63 6a 6e bf ec c5 f 8 7f 8d 6f aa
aa c 6f 12 4c 32 ec 1c 82 ed c6 89 b1 66 95 6f 1b 93 d 34 fc 9e 1 ad 18
3c 1f b1 4f f8 a1 9a 50 fc 67 4b 5a e4 95 c9 dc d5 8a 37 74 52 15 2d c5 dc
d5 9b d4 77 7c 5c 15 75 d9 e6 df a6 1c 43 8d 6c 86 b7 f9 6 cc 29 40 99 ee
6f c5 e4 3c 64 b1 68 9f d1 10 34 fa 26 1 f0 83 8e 6 ba 93 10 6e ad 5b 23
82 7 45 fc d 1b af e0 96 f7 b6 8f 95 39 80 7a 26 23 1b 60 f8 b3 d9 fd 1d
70 32 7f 96 aa b0 be a 24 43 3b 3a ea cb f1 cc bb 95 78 53 58 33 30 e fe
9a f3 a4 aa 65 5e 87 56 51 0 d3 67 f3 e5 af 64 c7 89 8e 40 c4 53 23 29 ed
51 df da b 57 d0 51 7e ed ed 58 d4 19 61 57 ec 66 ac 11 c4 61 c6 5d b4 7e
4 26 eb 5d 13 e4 c4 82 fe fd 86 6b 30 22 64 28 f2 10 ce 3 cf 7e a4 6a ba
11 d0 43 e3 9a b4 5e e4 91 5 3e da 44 2a aa b7 5e aa e 6 21 db 58 ad ee
ba 5a ed 93 9a 3b d5 4f 2 31 b8 5d b1 b7 d8 34 d8 21 54 47 f4 51 d1 b4 fd
7c d4 33 50 9c f7 14 d2 fc 30 76 ad 63 5c d2 f5 39 14 ff 79 7d f9 a0 99 e4
f0 35 80 38 8a 74 f5 34 d0 31 b4 73 d8 41 a6 4a b3 71 e0 5f ae 47 1e 91 81
23 e8 b8 2b 63 b4 81 4c c5 cd 40 29 eb 2b 6b e7 c7 80 d e 88 54 35 f3 b1
2a d0 17 62 50 a2 64 be c f9 37 c4 53 ae 5b a2 6a f7 49 ff 4d fe 92 af 8e
e8 7f 20 5c 76 89 f9 0 f8 1d 2e 78 bb 5a c7
diff --git a/puzzles/crypto/230/key b/puzzles/crypto/230/key new file mode 100644 index 0000000..b765c05 --- /dev/null +++ b/puzzles/crypto/230/key @@ -0,0 +1 @@ +quavering tendrils diff --git a/puzzles/crypto/230cbc.py b/puzzles/crypto/230cbc.py new file mode 100644 index 0000000..78afd30 --- /dev/null +++ b/puzzles/crypto/230cbc.py @@ -0,0 +1,38 @@ + +import cbc, crypto +import diffie + +alice = """Lets do a diffie hellman key exchange, Bob. The next puzzle will be encrypted using CBC and sha512(.) ^ text as the cipher function, +and an IV of 0xaa 64 times. The prime is: %d, mod: %d, and I chose %d. Also, have some more Lovecraft: Too awed even to hint theories, the seven shaking men trudged back toward Arkham by the north road. Ammi was worse than his fellows, and begged them to see him inside his own kitchen, instead of keeping straight on to town. He did not wish to cross the nighted, wind-whipped woods alone to his home on the main road. For he had had an added shock that the others were spared, and was crushed forever with a brooding fear he dared not even mention for many years to come. As the rest of the watchers on that tempestuous hill had stolidly set their faces toward the road, Ammi had looked back an instant at the shadowed valley of desolation so lately sheltering his ill-starred friend. And from that stricken, far-away spot he had seen something feebly rise, only to sink down again upon the place from which the great shapeless horror had shot into the sky. It was just a colourβ€”but not any colour of our earth or heavens. And because Ammi recognised that colour, and knew that this last faint remnant must still lurk down there in the well, he has never been quite right since. """ % \ +(diffie.prime, diffie.mod, diffie.a) +bob = """Umm, ok. You'll need this: %d. The key this time is 'quavering tendrils'. Some more text to decode: West of Arkham the hills rise wild, and there are valleys with deep woods that no axe has ever cut. There are dark narrow glens where the trees slope fantastically, and where thin brooklets trickle without ever having caught the glint of sunlight. On the gentler slopes there are farms, ancient and rocky, with squat, moss-coated cottages brooding eternally over old New England secrets in the lee of great ledges; but these are all vacant now, the wide chimneys crumbling and the shingled sides bulging perilously beneath low gambrel roofs. +The old folk have gone away, and foreigners do not like to live there. French-Canadians have tried it, Italians have tried it, and the Poles have come and departed. It is not because of anything that can be seen or heard or handled, but because of something that is imagined. The place is not good for the imagination, and does not bring restful dreams at night. It must be this which keeps the foreigners away, for old Ammi Pierce has never told them of anything he recalls from the strange days. Ammi, whose head has been a little queer for years, is the only one who still remains, or who ever talks of the strange days; and he dares to do this because his house is so near the open fields and the travelled roads around Arkham. +There was once a road over the hills and through the valleys, that ran straight where the blasted heath is now; but people ceased to use it and a new road was laid curving far toward the south. Traces of the old one can still be found amidst the weeds of a returning wilderness, and some of them will doubtless linger even when half the hollows are flooded for the new reservoir. Then the dark woods will be cut down and the blasted heath will slumber far below blue waters whose surface will mirror the sky and ripple in the sun. And the secrets of the strange days will be one with the deep’s secrets; one with the hidden lore of old ocean, and all the mystery of primal earth. +When I went into the hills and vales to survey for the new reservoir they told me the place was evil. They told me this in Arkham, and because that is a very old town full of witch legends I thought the evil must be something which grandams had whispered to children through centuries. The name β€œblasted heath” seemed to me very odd and theatrical, and I wondered how it had come into the folklore of a Puritan people. Then I saw that dark westward tangle of glens and slopes for myself, and ceased to wonder at anything besides its own elder mystery. It was morning when I saw it, but shadow lurked always there. The trees grew too thickly, and their trunks were too big for any healthy New England wood. There was too much silence in the dim alleys between them, and the floor was too soft with the dank moss and mattings of infinite years of decay. """ % \ +(diffie.B,) + +alice = crypto.strip(alice) +bob = crypto.strip(bob) + +def Ce(text, key): + out = bytearray() + for i in range(len(text)): + out.append( ( (text[i] + key[i]) % 256) ^ key[i] ) + + return bytes(out) + +def Cd(text, key): + out = bytearray() + for i in range(len(text)): + out.append( ( (text[i] ^ key[i]) - key[i]) % 256 ) + + return bytes(out) + +IV = b'ahiru' +key = b'space' + +encode = lambda t : cbc.cipherBlockChainingE(key, IV, Ce, t) +decode = lambda t : cbc.cipherBlockChainingD(key, IV, Cd, t) + +if __name__ == '__main__': + crypto.mkIndex(encode, decode, alice, bob, crypto.hexFormat) diff --git a/puzzles/crypto/240/index.mdwn b/puzzles/crypto/240/index.mdwn new file mode 100644 index 0000000..1abd607 --- /dev/null +++ b/puzzles/crypto/240/index.mdwn @@ -0,0 +1,2 @@ +
Alice
e3 75 ff 47 8e 96 a3 b 46 47 76 af a9 4 d6 13 99 65 79 d7 bd 3f 3a 5e f1
c4 29 f5 c 45 74 77 b6 84 4c dd 9 38 9c f6 bf a6 d6 ef 11 b5 22 b7 f7 b7
2 65 6b 84 7 3e 76 68 e3 d6 9b 3e 8a 95 92 a1 e6 a6 f9 a0 e4 ae aa a8 ab
bd ef fe b3 b2 b5 f0 b6 ae ee f0 e5 c3 ed ab e1 ef ee de b6 a0 f3 ec a2 a2
aa a9 a0 ae aa f8 fe a8 b7 aa aa ab bb f8 bb e2 b1 b0 ed b2 e7 ee f9 b7 ac
a0 bc a4 +
Bob
bb 29 86 18 a7 e0 a6 33 ec bc df 73 3e b5 dc 76 66 6d 74 68 30 81 9b 42 a1
41 ac 3b ee ab 3d ae d4 ae d0 33 d9 11 1 e4 57 3f 61 14 9b e2 85 f5 89 e0
cf 94 d8 56 71 a ec 99 7a 5d 4 7e 68 93
diff --git a/puzzles/crypto/240/key b/puzzles/crypto/240/key new file mode 100644 index 0000000..acc5b82 --- /dev/null +++ b/puzzles/crypto/240/key @@ -0,0 +1 @@ +in the same vein diff --git a/puzzles/crypto/240diffie.py b/puzzles/crypto/240diffie.py new file mode 100644 index 0000000..0120525 --- /dev/null +++ b/puzzles/crypto/240diffie.py @@ -0,0 +1,25 @@ +import crypto +import cbc +import diffie +import hashlib + +IV = [0xaa]*64 +aliceKey = hashlib.sha512(bytes('alice.%d' % diffie.key, 'utf-8')).digest() +bobKey = hashlib.sha512(bytes('bob.%d' % diffie.key, 'utf-8')).digest() + +alice = b"""Only one more puzzle to go. They'll never get it though, since we use a one time pad. I need to add more text here to pad this.""" +bob = b"""I wouldn't be so sure of that. The key is: in the same vein """ + +def C(text, key): + out = bytearray() + for i in range( len( text ) ): + out.append(key[i] ^ text[i]) + + return bytes(out) + +c = cbc.cipherBlockChainingE(aliceKey, IV, C, alice) +print('
Alice
', crypto.hexFormat(c)) +assert cbc.cipherBlockChainingD(aliceKey, IV, C, c) == alice +c = cbc.cipherBlockChainingE(bobKey, IV, C, bob) +assert cbc.cipherBlockChainingD(bobKey, IV, C, c) == bob +print('
Bob
', crypto.hexFormat(c), '
') diff --git a/puzzles/crypto/400/index.mdwn b/puzzles/crypto/400/index.mdwn new file mode 100644 index 0000000..f57c9a7 --- /dev/null +++ b/puzzles/crypto/400/index.mdwn @@ -0,0 +1,2 @@ +
Alice
f4 c9 40 41 ac 4b 9c 3f 32 58 2f 70 1c 49 fb bf a8 56 72 72 2 88 2c 87 cc
d 13 6c 25 d5 da 30 64 dd dd b8 ba 58 c1 a3 17 26 6d ff 64 62 cb 69 b3 e2
ae 2d dd 11 2f a1 5d 79 b6 63 cb 51 5b de 9c 57 20 45 72 7b f2 35 15 40 60
8c 45 c9 a6 38 e0 79 7 a4 cc 18 e9 7e eb 4b 38 e2 ed 6b 17 a1 ee fd 69 2a
24 b5 21 be 96 92 d4 f6 5b 40 59 1d b f6 8a cb dd 6 43 16 6 f ab c8 4
fc b2 f3 c3 64 11 40 db 9e d6 7 f9 40 17 bd 2 1e cc b2 14 81 6a c1 6b b9
2c 6c ab 5f 7f +
Bob
e9 d5 6 46 ac 52 92 38 32 5d 32 37 59 10 e3 af a8 46 72 6b 18 89 68 c2 c0
15 13 2f 3d 94 d0 2b 31 9e db ae ea 5f c1 ef 11 36 37 e4 66 7e 9e 21 99 e3
a7 6a ce 13 2f a1 50 74 ae 73 84 5b 41 96 da 1e a 10 76 60 a2 39 5b 19 6e
d9 1 da ba 75 ac 71 1b b5 89 1e a1 7e f3 b 36 ab a3 6b 43 e9 ab fd 23 75
68 f6 68 b3 c8 dc 81 ae b 18 44 4f 50 eb aa cb d5 6 4f 16 a 49 b0 d2 41
df bb 93 cd 7d d 42 c6 cc 8d 1a b0 f 43 f6 46 52 8d bf 5d c5 21 8a 22 fa
79 20 ff 6 71
diff --git a/puzzles/crypto/400/key b/puzzles/crypto/400/key new file mode 100644 index 0000000..0abb17e --- /dev/null +++ b/puzzles/crypto/400/key @@ -0,0 +1 @@ +--------========Thanks for Pl@y|ng========-------- diff --git a/puzzles/crypto/400onetimepad.py b/puzzles/crypto/400onetimepad.py new file mode 100644 index 0000000..a9a589b --- /dev/null +++ b/puzzles/crypto/400onetimepad.py @@ -0,0 +1,23 @@ +import crypto +import random + +def mkPad(length): + pad = bytearray() + for i in range(length): + pad.append( random.randint(0,255) ) + return bytes(pad) + +alice = b'That was it, you solved the last crypto puzzle! Congratulations. I hope you realize that, in the grand scheme of things, these were of trivial difficulty.' +bob = b"It's not like we could expect you to solve anything actually difficult in a day, after all. --------========Thanks for Pl@y|ng========-------- " + +assert len(alice) == len(bob) +key = mkPad(len(alice)) + +def encode(text): + out = bytearray() + for i in range(len(text)): + out.append(key[i] ^ text[i]) + return bytes(out) + +crypto.mkIndex(encode, encode, alice, bob, crypto.hexFormat) + diff --git a/puzzles/crypto/cbc.py b/puzzles/crypto/cbc.py new file mode 100644 index 0000000..d63f3d1 --- /dev/null +++ b/puzzles/crypto/cbc.py @@ -0,0 +1,52 @@ + +def cipherBlockChainingE(key, IV, C, text): + """Cypher block chaining encryption. Works in blocks the size of IV. +@param key: the key for the Cipher. +@param IV: initialization vector (bytes object). +@param C: the cypher function C(text, key). +@param text: A bytes object of the text. The length of the text + must be a multiple of the length of the IV. +""" + mod = len(text) % len(IV) + assert mod == 0, 'The text length needs to be a multiple of the key '\ + 'length. %d of %d' % (mod, len(IV)) + + feedback = IV + block = len(IV) + out = bytearray() + while text: + p, text = text[:block], text[block:] + + c = bytearray(block) + for i in range(block): + c[i] = p[i] ^ feedback[i] + + c2 = C(c, key) + out.extend(c2) + feedback = c2 + + return bytes(out) + +def cipherBlockChainingD(key, IV, C, text): + """Cipher block chaining decryption. Arguments are the same as for the +encrypting function.""" + mod = len(text) % len(IV) + assert mod == 0, 'The text length needs to be a multiple of the IV '\ + 'length. %d of %d' % (mod, len(IV)) + + feedback = IV + block = len(IV) + out = bytearray() + while text: + c, text = text[:block], text[block:] + + p = C(c, key) + p = bytearray(p) + for i in range(block): + p[i] = p[i] ^ feedback[i] + + out.extend(p) + feedback = c + + return bytes(out) + diff --git a/puzzles/crypto/crypto.py b/puzzles/crypto/crypto.py new file mode 100644 index 0000000..9f0f6f4 --- /dev/null +++ b/puzzles/crypto/crypto.py @@ -0,0 +1,46 @@ +def mkIndex(encode, decode, alice, bob, + format=lambda s: str(s, 'utf-8')): + """Write out the index.html contents. +@param encode: function to encrypt the plaintext +@param decode: function to decrypt the plaintext +@param alice: plaintext of alice line +@param bob: plaintext of bob line +@param format: formatter for the cypher text, run out output of encode before + printing. Does string conversion by default.""" + c = encode(alice) + print('
Alice
', format(c)) + assert decode(c) == alice + c = encode(bob) + print('
Bob
', format(c), '
') + assert decode(c) == bob + +def hexFormat(text): + return groups(text, 5, '{0:x} ') + +def groups(text, perLine=5, format='{0:c}'): + i = 0 + out = [] + while i < len(text): + out.append(format.format(text[i])) + + if i % (perLine*5) == (perLine * 5 - 1): + out.append('
') + elif i % 5 == 4: + out.append(' ') + + i = i + 1 + + return ''.join(out) + +def strip(text): + """Strip any unicode from the given text, and return it as a bytes + object.""" + + b = bytearray() + for t in text: + if ord(t) > 255: + t = ' ' + + b.append(ord(t)) + + return bytes(b) diff --git a/puzzles/crypto/diffie.py b/puzzles/crypto/diffie.py new file mode 100644 index 0000000..5f0bbba --- /dev/null +++ b/puzzles/crypto/diffie.py @@ -0,0 +1,8 @@ +prime = 51237129793 +mod = 321454621 +a = 341 +A = prime ** a % mod +b = 573 +B = prime ** b % mod +key = A**b % mod +assert B**a % mod == key, 'Bad diffie math.' diff --git a/puzzles/crypto/transform.py b/puzzles/crypto/transform.py new file mode 100644 index 0000000..d9ffe1b --- /dev/null +++ b/puzzles/crypto/transform.py @@ -0,0 +1,13 @@ +def transform(text, map): + size = len(map) + div = len(text) % size + assert div == 0, 'Text must be a multiple of the key size in length. '\ + 'At %d out of %d' % (div, size) + + out = bytearray() + i = 0 + while i < len(text): + for j in range(size): + out.append( text[i + map[j]] ) + i = i+size + return bytes(out) diff --git a/puzzles/forensics/10/index.html b/puzzles/forensics/10/index.html new file mode 100755 index 0000000..5b442e2 --- /dev/null +++ b/puzzles/forensics/10/index.html @@ -0,0 +1,13 @@ + +You have suspicions that a certain windows box has been infected by a Trojan. You have been given access to a memory image from this box.xp-laptop-2005-06-25.img Use the memory image to determine if the machine has been infected. +
+In order to answer the questions: +
+ - Determine if the machine has been infected. +
+ - If it has not been infected, list "no" as your answer. +
+ - If it has been infected, list the process name of the Trojan +
+HINT: You know from googling that the Trojan uses the passWD.log file. + diff --git a/puzzles/forensics/10/key b/puzzles/forensics/10/key new file mode 100755 index 0000000..97d62ab --- /dev/null +++ b/puzzles/forensics/10/key @@ -0,0 +1 @@ +lsass.exe diff --git a/puzzles/forensics/100/index.html b/puzzles/forensics/100/index.html new file mode 100755 index 0000000..bd74257 --- /dev/null +++ b/puzzles/forensics/100/index.html @@ -0,0 +1,2 @@ +What is the method of attack? +image file \ No newline at end of file diff --git a/puzzles/forensics/100/key b/puzzles/forensics/100/key new file mode 100755 index 0000000..30383c8 --- /dev/null +++ b/puzzles/forensics/100/key @@ -0,0 +1 @@ +dll injection \ No newline at end of file diff --git a/puzzles/forensics/20/index.html b/puzzles/forensics/20/index.html new file mode 100755 index 0000000..94c6203 --- /dev/null +++ b/puzzles/forensics/20/index.html @@ -0,0 +1,15 @@ + + +You are currently employed as a SW engineer at KELCY INC. One of your clients has informed you that $10,000 has been deducted from their accounts from an authorized user. They have delivered a software image for you to investigate. Determine if the machine has been compromised. +
+In order to answer the questions: +
+ - Determine if the machine has been compromised. +
+ - If it has not been compromised, list "no" as your answer. +
+ - If it has been compromised, list the file name (with its extension) being used by the malicious software +
+winxppro.vmem + + diff --git a/puzzles/forensics/20/key b/puzzles/forensics/20/key new file mode 100755 index 0000000..8c72a46 --- /dev/null +++ b/puzzles/forensics/20/key @@ -0,0 +1 @@ +klog.txt diff --git a/puzzles/forensics/200/index.html b/puzzles/forensics/200/index.html new file mode 100755 index 0000000..e137f68 --- /dev/null +++ b/puzzles/forensics/200/index.html @@ -0,0 +1,2 @@ +What is the name of what was injected? +image file \ No newline at end of file diff --git a/puzzles/forensics/200/key b/puzzles/forensics/200/key new file mode 100755 index 0000000..bda2bd0 --- /dev/null +++ b/puzzles/forensics/200/key @@ -0,0 +1 @@ +winsecur.dll \ No newline at end of file diff --git a/puzzles/forensics/250/index.html b/puzzles/forensics/250/index.html new file mode 100755 index 0000000..9937b0b --- /dev/null +++ b/puzzles/forensics/250/index.html @@ -0,0 +1,10 @@ +SA Dumas from the Albuquerque FBI Cyber Squad has alerted you that Antoniette Balls (Iranian postdoc with a username of "aballs@tipmeover.org") working at the lab has been in contact with Iranian Jihad organization. Find the code that she is transmitting to the Iranian Jihad Organization. +AD database +
+
+To: Help Desk, +Subject: Here is the .dit file for the domain controller as requested. Let me know if you need anything else. + +Ask for Gary: +505.452.6718 +505.280.8668 \ No newline at end of file diff --git a/puzzles/forensics/250/key b/puzzles/forensics/250/key new file mode 100755 index 0000000..3d9c36f --- /dev/null +++ b/puzzles/forensics/250/key @@ -0,0 +1 @@ +Dirka Dirka \ No newline at end of file diff --git a/puzzles/forensics/400/index.html b/puzzles/forensics/400/index.html new file mode 100755 index 0000000..39c5b98 --- /dev/null +++ b/puzzles/forensics/400/index.html @@ -0,0 +1,12 @@ + +Determine which file has been winrared in this archive. +
+
+In order to answer the questions: +
+ - List the file name (including extension) of the file that has been winrared +
+image file +rar file + + diff --git a/puzzles/forensics/400/key b/puzzles/forensics/400/key new file mode 100755 index 0000000..9ab5189 --- /dev/null +++ b/puzzles/forensics/400/key @@ -0,0 +1 @@ +avatar.txt diff --git a/puzzles/forensics/50/index.html b/puzzles/forensics/50/index.html new file mode 100755 index 0000000..a79d0e8 --- /dev/null +++ b/puzzles/forensics/50/index.html @@ -0,0 +1,11 @@ + +You know that a machine has been comprimised. There is a malicious piece of software that logs all key strokes from a computer's keyboard. Find the driver associated with the malicious piece of software. +
+
+In order to answer the questions: +
+ - List the driver name with its full path +
+winxppro.vmem + + diff --git a/puzzles/forensics/50/key b/puzzles/forensics/50/key new file mode 100755 index 0000000..d4e3038 --- /dev/null +++ b/puzzles/forensics/50/key @@ -0,0 +1 @@ +C:\WINDOWS\system32\klog.sys diff --git a/puzzles/hackme/100/index.mdwn b/puzzles/hackme/100/index.mdwn new file mode 100644 index 0000000..b99f752 --- /dev/null +++ b/puzzles/hackme/100/index.mdwn @@ -0,0 +1 @@ +There is no key. It is impossible to score 100 points in this category. diff --git a/puzzles/hackme/200/index.mdwn b/puzzles/hackme/200/index.mdwn new file mode 100644 index 0000000..b4b4218 --- /dev/null +++ b/puzzles/hackme/200/index.mdwn @@ -0,0 +1,3 @@ +The key is "james". Should you claim your points immediately, though? +Go ahead, watch what happens when you do. + diff --git a/puzzles/hackme/200/key b/puzzles/hackme/200/key new file mode 100644 index 0000000..dc065e0 --- /dev/null +++ b/puzzles/hackme/200/key @@ -0,0 +1 @@ +james diff --git a/puzzles/hackme/274/index.mdwn b/puzzles/hackme/274/index.mdwn new file mode 100644 index 0000000..72dd672 --- /dev/null +++ b/puzzles/hackme/274/index.mdwn @@ -0,0 +1,2 @@ +So was it a good idea to claim your points as soon as possible? +Watch the scoreboard. diff --git a/puzzles/hackme/300/index.mdwn b/puzzles/hackme/300/index.mdwn new file mode 100644 index 0000000..bcb3c57 --- /dev/null +++ b/puzzles/hackme/300/index.mdwn @@ -0,0 +1 @@ +You're going to have to work harder now. diff --git a/puzzles/hackme/614/index.mdwn b/puzzles/hackme/614/index.mdwn new file mode 100644 index 0000000..dc065e0 --- /dev/null +++ b/puzzles/hackme/614/index.mdwn @@ -0,0 +1 @@ +james diff --git a/puzzles/hackme/614/key b/puzzles/hackme/614/key new file mode 100644 index 0000000..dc065e0 --- /dev/null +++ b/puzzles/hackme/614/key @@ -0,0 +1 @@ +james diff --git a/puzzles/hackme/806/index.mdwn b/puzzles/hackme/806/index.mdwn new file mode 100644 index 0000000..dd3d17d --- /dev/null +++ b/puzzles/hackme/806/index.mdwn @@ -0,0 +1 @@ +That's all, folks. diff --git a/puzzles/hackme/806/key b/puzzles/hackme/806/key new file mode 100644 index 0000000..dd3d17d --- /dev/null +++ b/puzzles/hackme/806/key @@ -0,0 +1 @@ +That's all, folks. diff --git a/puzzles/hackme/summary.txt b/puzzles/hackme/summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/puzzles/hispaniola/10/img.png b/puzzles/hispaniola/10/img.png new file mode 100644 index 0000000..2d4e686 Binary files /dev/null and b/puzzles/hispaniola/10/img.png differ diff --git a/puzzles/hispaniola/10/index.mdwn b/puzzles/hispaniola/10/index.mdwn new file mode 100644 index 0000000..2c21f28 --- /dev/null +++ b/puzzles/hispaniola/10/index.mdwn @@ -0,0 +1 @@ + diff --git a/puzzles/hispaniola/10/key b/puzzles/hispaniola/10/key new file mode 100644 index 0000000..071491e --- /dev/null +++ b/puzzles/hispaniola/10/key @@ -0,0 +1 @@ +You're well on your way :) diff --git a/puzzles/hispaniola/125/index.mdwn b/puzzles/hispaniola/125/index.mdwn new file mode 100644 index 0000000..90ccc06 --- /dev/null +++ b/puzzles/hispaniola/125/index.mdwn @@ -0,0 +1 @@ +Puzzle:  Braille diff --git a/puzzles/hispaniola/125/key b/puzzles/hispaniola/125/key new file mode 100644 index 0000000..6c456ba --- /dev/null +++ b/puzzles/hispaniola/125/key @@ -0,0 +1 @@ +β€½ diff --git a/puzzles/hispaniola/15/img.png b/puzzles/hispaniola/15/img.png new file mode 100644 index 0000000..ea2737b Binary files /dev/null and b/puzzles/hispaniola/15/img.png differ diff --git a/puzzles/hispaniola/15/index.mdwn b/puzzles/hispaniola/15/index.mdwn new file mode 100644 index 0000000..067714f --- /dev/null +++ b/puzzles/hispaniola/15/index.mdwn @@ -0,0 +1 @@ + diff --git a/puzzles/hispaniola/15/key b/puzzles/hispaniola/15/key new file mode 100644 index 0000000..1941358 --- /dev/null +++ b/puzzles/hispaniola/15/key @@ -0,0 +1 @@ +-462766 diff --git a/puzzles/hispaniola/5/img.png b/puzzles/hispaniola/5/img.png new file mode 100644 index 0000000..ad0dbc9 Binary files /dev/null and b/puzzles/hispaniola/5/img.png differ diff --git a/puzzles/hispaniola/5/index.mdwn b/puzzles/hispaniola/5/index.mdwn new file mode 100644 index 0000000..7e4e1e7 --- /dev/null +++ b/puzzles/hispaniola/5/index.mdwn @@ -0,0 +1 @@ + diff --git a/puzzles/hispaniola/5/key b/puzzles/hispaniola/5/key new file mode 100644 index 0000000..9b8d504 --- /dev/null +++ b/puzzles/hispaniola/5/key @@ -0,0 +1 @@ +3acd767f2717b84076cdcd18e882f01d diff --git a/puzzles/hispaniola/50/index.mdwn b/puzzles/hispaniola/50/index.mdwn new file mode 100644 index 0000000..45484e9 --- /dev/null +++ b/puzzles/hispaniola/50/index.mdwn @@ -0,0 +1,9 @@ +Little-known fact is that the
+Answer is right here.
+Now all you have to do is
+Look around and you'll know where you need to
+ +Go. You can't continue
+Unless you determine this key.
+You'll need to enter it here and
+Speak it to obtain the next item in the treasure hunt.
diff --git a/puzzles/hispaniola/50/key b/puzzles/hispaniola/50/key new file mode 100644 index 0000000..c1783d9 --- /dev/null +++ b/puzzles/hispaniola/50/key @@ -0,0 +1 @@ +LANL GUYS diff --git a/puzzles/hispaniola/75/index.mdwn b/puzzles/hispaniola/75/index.mdwn new file mode 100644 index 0000000..dc8e55c --- /dev/null +++ b/puzzles/hispaniola/75/index.mdwn @@ -0,0 +1,3 @@ +Give the entire sequence. The symbols are here, in no particular order, for you to copy and paste.
+Make sure to include spaces between symbols (but no leading or trailing spaces).

+◕ ⚑ ♥ ◢ ★ diff --git a/puzzles/hispaniola/75/key b/puzzles/hispaniola/75/key new file mode 100644 index 0000000..576d8a7 --- /dev/null +++ b/puzzles/hispaniola/75/key @@ -0,0 +1 @@ +βš‘ β—’ β—• β˜… β™₯ β—’ β™₯ βš‘ β—• β˜… β™₯ β—• β˜… β™₯ βš‘ β˜… βš‘ β—’ β™₯ β—’ β—• β—• β—’ β˜… βš‘ diff --git a/puzzles/hispaniola/summary.txt b/puzzles/hispaniola/summary.txt new file mode 100644 index 0000000..77d99e7 --- /dev/null +++ b/puzzles/hispaniola/summary.txt @@ -0,0 +1 @@ +The "hispaniola" category requires contenstants to treasure-hunt for tangible items to learn the keys to each puzzle. diff --git a/puzzles/net-re/.gitignore b/puzzles/net-re/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/puzzles/net-re/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/puzzles/net-re/1/index.mdwn b/puzzles/net-re/1/index.mdwn new file mode 100644 index 0000000..cc40c76 --- /dev/null +++ b/puzzles/net-re/1/index.mdwn @@ -0,0 +1,95 @@ +Decimal +======= + +Decimal (Greek β€œdeca-”, meaning β€œten”), or base ten, is the counting +system most people use regularly, consisting of ten digits: + + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + +To count past nine, one must use the β€œtens place”, starting by placing a +1 in the β€œtens place”: + + 10, 11, 12, 13, 14, 15, … + +The presence of a 1 in the β€œtens place” means β€œadd ten one time”. 2 in +the β€œtens place” would mean β€œadd ten two times", and so on. The decimal +number: + + 372 + +Reads β€œthree times one hundred, plus seven times ten, plus two”. + +A β€œhundreds place” and β€œthousands place” can exist as well, and so on +for any nth digit representing the place for ten times itself n times, +or 10ⁿ. + + +Octal +===== + +Octal (Greek β€œokta-” meaning β€œeight”), or base eight, is a method of +counting with only eight digits. In the interest of making it obvious +when a number is octal, all octal numbers on this page will be preceded +by the number β€œ0”. The digits are thus: + + 00, 01, 02, 03, 04, 05, 06, 07 + +Octal features an β€œeights place”, a β€œsixty-fours place”, a β€œfive hundred +twelves place”, and so on. Counting from zero to ten in octal would +therefore go like so: + + 00, 01, 02, 03, 04, 05, 06, 07, 010, 011, 012 + +Where β€œ010” means β€œone times eight” (eight), β€œ011” means β€œone times +eight plus one” (nine), and β€œ012” means β€œone times eight plus two” +(ten). + +The octal number: + + 0372 + +Reads β€œthree times sixty-four, plus seven times eight, plus two”, or two +hundred thirty four. + +In general, the nth digit of an octal number represents the place for +eight times itself n times, or 8ⁿ. + + +Hexadecimal +=========== + +Hexadecimal (Greek β€œhexa-”, meaning β€œsix”; plus β€œdeca-”, meaning ten), +or base sixteen, is a method of counting with sixteen digits (preceded +by β€œ0x” on this page): + + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, + 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF + +Hexadecimal (or β€œhex”) features a β€œsixteens place”, a β€œtwo hundred fifty +sixes place”, and so on. Counting from zero to twenty in hex therefore +would go like so: + + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, + 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, + 0x10, 0x11, 0x12, 0x13, 0x14 + +Where β€œ0xA” means β€œten”, β€œ0xB” means β€œeleven”, β€œ0xF” means β€œfifteen”, β€œ0x10” +means β€œone times sixteen”, β€œ0x11” means β€œone times sixteen plus one”, et +cetera. + +The hexadecimal number: + + 0x372 + +Reads β€œthree times two hundred fifty six, plus seven times sixteen, plus +two”, or eight hundred eighty-two. + + + +Question +======== + +The key for this page is the decimal representation of the following +sum: + + 12 + 072 + 0x5D diff --git a/puzzles/net-re/1/key b/puzzles/net-re/1/key new file mode 100644 index 0000000..9cc2bc3 --- /dev/null +++ b/puzzles/net-re/1/key @@ -0,0 +1 @@ +163 diff --git a/puzzles/net-re/10/index.mdwn b/puzzles/net-re/10/index.mdwn new file mode 100644 index 0000000..8c91b7e --- /dev/null +++ b/puzzles/net-re/10/index.mdwn @@ -0,0 +1 @@ +What was the close price for Exxon-Mobil? diff --git a/puzzles/net-re/10/key b/puzzles/net-re/10/key new file mode 100644 index 0000000..a564e04 --- /dev/null +++ b/puzzles/net-re/10/key @@ -0,0 +1 @@ +69.35 diff --git a/puzzles/net-re/10/session.pcap b/puzzles/net-re/10/session.pcap new file mode 100644 index 0000000..ca71898 Binary files /dev/null and b/puzzles/net-re/10/session.pcap differ diff --git a/puzzles/net-re/100/index.mdwn b/puzzles/net-re/100/index.mdwn new file mode 100644 index 0000000..3598a27 --- /dev/null +++ b/puzzles/net-re/100/index.mdwn @@ -0,0 +1,61 @@ + 4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f + 6e7365637465747572206164697069736963696e6720656c69742c207365 + 6420646f20656975736d6f642074656d706f7220696e6369646964756e74 + 207574206c61626f726520657420646f6c6f7265206d61676e6120616c69 + 7175612e20557420656e696d206164206d696e696d2076656e69616d2c20 + 71756973206e6f737472756420657865726369746174696f6e20756c6c61 + 6d636f206c61626f726973206e69736920757420616c6971756970206578 + 20656120636f6d6d6f646f20636f6e7365717561742e2044756973206175 + 746520697275726520646f6c6f7220696e20726570726568656e64657269 + 7420696e20766f6c7570746174652076656c697420657373652063696c6c + 756d20646f6c6f726520657520667567696174206e756c6c612070617269 + 617475722e204578636570746575722073696e74206f6363616563617420 + 637570696461746174206e6f6e2070726f6964656e742c2073756e742069 + 6e2063756c706120717569206f666669636961206465736572756e74206d + 6f6c6c697420616e696d20696420657374206c61626f72756d2e0a4c6f72 + 656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365 + 637465747572206164697069736963696e6720656c69742c207365642064 + 6f20656975736d6f642074656d706f7220696e6369646964756e74207574 + 206c61626f726520657420646f6c6f7265206d61676e6120616c69717561 + 2e20557420656e696d206164206d696e696d2076656e69616d2c20717569 + 73206e6f737472756420657865726369746174696f6e20756c6c616d636f + 206c61626f726973206e69736920757420616c6971756970206578206561 + 20636f6d6d6f646f20636f6e7365717561742e2044756973206175746520 + 697275726520646f6c6f7220696e20726570726568656e64657269742069 + 6e20766f6c7570746174652076656c697420657373652063696c6c756d20 + 646f6c6f726520657520667567696174206e756c6c612070617269617475 + 722e204578636570746575722073696e74206f6363616563617420637570 + 696461746174206e6f6e2070726f6964656e742c2073756e7420696e2063 + 756c706120717569206f666669636961206465736572756e74206d6f6c6c + 697420616e696d20696420657374206c61626f72756d2e0a546865206b65 + 7920697320226368756d6275636b6574222e0a4c6f72656d20697073756d + 20646f6c6f722073697420616d65742c20636f6e73656374657475722061 + 64697069736963696e6720656c69742c2073656420646f20656975736d6f + 642074656d706f7220696e6369646964756e74207574206c61626f726520 + 657420646f6c6f7265206d61676e6120616c697175612e20557420656e69 + 6d206164206d696e696d2076656e69616d2c2071756973206e6f73747275 + 6420657865726369746174696f6e20756c6c616d636f206c61626f726973 + 206e69736920757420616c697175697020657820656120636f6d6d6f646f + 20636f6e7365717561742e2044756973206175746520697275726520646f + 6c6f7220696e20726570726568656e646572697420696e20766f6c757074 + 6174652076656c697420657373652063696c6c756d20646f6c6f72652065 + 7520667567696174206e756c6c612070617269617475722e204578636570 + 746575722073696e74206f6363616563617420637570696461746174206e + 6f6e2070726f6964656e742c2073756e7420696e2063756c706120717569 + 206f666669636961206465736572756e74206d6f6c6c697420616e696d20 + 696420657374206c61626f72756d2e0a4c6f72656d20697073756d20646f + 6c6f722073697420616d65742c20636f6e73656374657475722061646970 + 69736963696e6720656c69742c2073656420646f20656975736d6f642074 + 656d706f7220696e6369646964756e74207574206c61626f726520657420 + 646f6c6f7265206d61676e6120616c697175612e20557420656e696d2061 + 64206d696e696d2076656e69616d2c2071756973206e6f73747275642065 + 7865726369746174696f6e20756c6c616d636f206c61626f726973206e69 + 736920757420616c697175697020657820656120636f6d6d6f646f20636f + 6e7365717561742e2044756973206175746520697275726520646f6c6f72 + 20696e20726570726568656e646572697420696e20766f6c757074617465 + 2076656c697420657373652063696c6c756d20646f6c6f72652065752066 + 7567696174206e756c6c612070617269617475722e204578636570746575 + 722073696e74206f6363616563617420637570696461746174206e6f6e20 + 70726f6964656e742c2073756e7420696e2063756c706120717569206f66 + 6669636961206465736572756e74206d6f6c6c697420616e696d20696420 + 657374206c61626f72756d2e \ No newline at end of file diff --git a/puzzles/net-re/100/key b/puzzles/net-re/100/key new file mode 100644 index 0000000..cf47e71 --- /dev/null +++ b/puzzles/net-re/100/key @@ -0,0 +1 @@ +chumbucket diff --git a/puzzles/net-re/1000/index.mdwn b/puzzles/net-re/1000/index.mdwn new file mode 100644 index 0000000..52b1f57 --- /dev/null +++ b/puzzles/net-re/1000/index.mdwn @@ -0,0 +1,26 @@ + 00000000 25 45 05 05 0f 08 0e 43 02 05 00 48 4b 08 0c 4d + 00000010 15 49 49 0b 1f 12 0c 43 0d 03 06 00 00 00 00 52 + 00000020 6b 1d 59 54 5b 5b 54 5e 48 51 49 1d 56 58 44 1c + 00000030 5c 1d 59 54 5b 5b 54 5e 48 51 49 1d 56 58 44 1c + 00000040 5c 2a 6e 28 12 46 1a 0c 18 09 54 50 04 0c 17 55 + 00000050 41 59 0b 1c 46 0b 08 1a 55 0a 1d 4e 0f 45 00 4e + 00000060 14 52 17 0c 0a 00 49 0b 14 1a 1d 4e 0c 45 0d 4e + 00000070 41 43 0b 07 12 03 07 07 55 1b 1d 54 03 45 01 4e + 00000080 13 00 0f 0c 1f 15 49 17 1d 0d 00 2a 0a 17 1c 01 + 00000090 0d 4f 0a 0e 03 14 49 17 1d 0d 1a 00 0a 45 0a 48 + 000000a0 0f 47 08 0c 46 04 10 17 10 42 54 00 3f 0d 1c 52 + 000000b0 04 00 07 08 08 46 0b 06 55 18 06 49 08 0e 00 0d + 000000c0 41 42 11 1d 46 02 06 0d 52 18 54 4c 0e 11 59 55 + 000000d0 09 45 09 63 02 0f 1a 00 1a 19 06 41 0c 00 59 58 + 000000e0 0e 55 4a 49 46 24 10 43 1b 03 03 00 12 0a 0c 01 + 000000f0 00 4c 16 0c 07 02 10 43 1d 0d 02 45 4b 04 15 4d + 00000100 41 54 0c 0c 46 12 06 0c 19 1f 54 59 04 10 59 4f + 00000110 04 45 00 49 12 09 49 00 07 0d 17 4b 61 11 11 44 + 00000120 0c 0e 44 49 2f 12 49 17 14 07 11 53 4b 04 59 4d + 00000130 08 54 10 05 03 46 00 0d 06 05 13 48 1f 45 18 4f + 00000140 05 00 05 49 0a 09 1d 43 1a 0a 54 50 0a 11 10 44 + 00000150 0f 43 01 47 6c 6c 44 4e 58 41 59 0d 46 48 54 0c + 00000160 4c 0d 49 63 6c 32 01 06 55 07 11 59 4b 03 16 53 + 00000170 41 54 0c 00 15 46 19 02 12 09 54 49 18 45 5b 53 + 00000180 14 47 06 10 46 17 1c 02 13 0a 56 2a + 0000018c diff --git a/puzzles/net-re/1000/key b/puzzles/net-re/1000/key new file mode 100644 index 0000000..cbd06d9 --- /dev/null +++ b/puzzles/net-re/1000/key @@ -0,0 +1 @@ +a difficult key! \ No newline at end of file diff --git a/puzzles/net-re/1200/14b44ef250afd460b42d0947846b306e b/puzzles/net-re/1200/14b44ef250afd460b42d0947846b306e new file mode 100644 index 0000000..9b16cb8 --- /dev/null +++ b/puzzles/net-re/1200/14b44ef250afd460b42d0947846b306e @@ -0,0 +1 @@ +gd{7W¨»ηΐΫ8`p{HW¨»ηΐΫ8`g{HW¨»Ί»ΏΫ8gG`;HW¨Δο˜ΐΫdD7WΧ»ηΏΫ8g{bW¨η»ΐ‡8gg{7WΤ»ΜηœΤ8d`p'H ¨η»ΐΤ8hgGJgp'H ¨η»ΐ‡8K{W¨»Œηΐ€GG;p'H ¨Ηο˜ΐ§G;p'7 ¨ηο»Ό€GhD<,7+ΧΔο»Ό€GG`;'7(Χ΄ΊηΐΫ8`gp{HW¨»ηΐΫ8`gp{HWτΔο˜ΟΫ8`gp{bWΧ»οηΏΫ8`gp{HWΧ»ηΐΫ8g`gp{HW¨»ηΏΫ82`;prH ΧĐηΐ€GhG`;{7(Χ琻Ώ€8dp'H Χ»ΊηΆΫNKg {GWΧ»μ»ΐ€Gd;p|7WΤ»ŸηΏΫDKd;Z{HW¨η»ΐ‡8Knp'H ΧΗο˜ΐ§8KG`;ps7^¨η»ΏΫ`;'H Χη옿€7dd;7XΧ琻Ώ‡Dhgdog 7 ‚»ηΐΫ8`gp{HW¨»ηΐΫ8`gp{HW¨»ηΐΫ8Jg{HW¨»ηΐΫ8`gp{HW¨»ηΐ€8`gpH(¨»Ί»ΐ‡GgdgpH(׻Ϋ8gd`,{(¨Δο˜ΘΫdJ;p7X¨Δ›œΫ?h[`pHX¨ΔΠηœΫGhg H!¨»Ί»ΐ‡ddig,{Wτ»ΜηœΫdd N ^ n ~ + F: ␏ ␟ / ? O _ o ␑ + +Characters 0x00 through 0x1F and character 0x7F are the so-called +"control characters" and are typically not displayed. + +So, the following sequence of hex values: + + 68 65 6C 6C 6F 20 77 6F 72 6C 64 2E + +when decoded as ASCII yields the string: + + hello world + + +Base 64 +======= + +Base 64 is a map of 6-bit values--0x00 through 0x3F--to ASCII +characters. Here is the Base 64 alphabet: + + 0 1 2 3 + --------- + 0: A Q g w + 1: B R h x + 2: C S i y + 3: D T j z + 4: E U k 0 + 5: F V l 1 + 6: G W m 2 + 7: H X n 3 + 8: I Y o 4 + 9: J Z p 5 + A: K a q 6 + B: L b r 7 + C: M c s 8 + D: N d t 9 + E: O e u + + F: P f v / + +The following sequence of hex values: + + 00 00 12 2F 3C 07 + +when encoded as Base 64 yields the ASCII string: + + AASv8H + + +6-bit values in an 8-bit world +============================== + +Modern computers, almost universally, use an 8-bit byte. Base64 is used +to map an 8-bit byte stream to a set of 64 universally printable +characters. + +In order to do this, the 8-bit *byte* stream must be converted into a +*bit* stream. This bit stream must then be converted into a stream of +6-bit bytes, as in the following figure: + + | 00 | 01 | AF | F0 | B4 | 14 | + | | | | | | | + |000000 00|0000 1000|10 101111|111100 00|0111 0100|00 010100| + | | + |000000|00 0000|1000 10|101111|111100|00 0111|0100 00|010100| + | | | | | | | | | + | 00 | 00 | 12 | 2F | 3C | 07 | 10 | 14 | + +The resulting 6-bit byte stream is then mapped into the ASCII alphabet +given in the previous section, resulting in a "Base64-encoded string": + + AASv8HQU + + +Question +======== + +The key for this page is the decode of this Base64-encoded string: + + Z3JlYXQgam9i diff --git a/puzzles/net-re/2/key b/puzzles/net-re/2/key new file mode 100644 index 0000000..c342710 --- /dev/null +++ b/puzzles/net-re/2/key @@ -0,0 +1 @@ +great job diff --git a/puzzles/net-re/20/index.mdwn b/puzzles/net-re/20/index.mdwn new file mode 100644 index 0000000..20e6ee0 --- /dev/null +++ b/puzzles/net-re/20/index.mdwn @@ -0,0 +1 @@ +What sort of creature is Domo's friend? diff --git a/puzzles/net-re/20/key b/puzzles/net-re/20/key new file mode 100644 index 0000000..e29af1d --- /dev/null +++ b/puzzles/net-re/20/key @@ -0,0 +1 @@ +squirrel diff --git a/puzzles/net-re/20/session.pcap b/puzzles/net-re/20/session.pcap new file mode 100644 index 0000000..5317aa8 Binary files /dev/null and b/puzzles/net-re/20/session.pcap differ diff --git a/puzzles/net-re/200/index.mdwn b/puzzles/net-re/200/index.mdwn new file mode 100644 index 0000000..6bee4ad --- /dev/null +++ b/puzzles/net-re/200/index.mdwn @@ -0,0 +1,32 @@ + TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2ljaW5nIGVsaXQs + IHNlZCBkbyBlaXVzbW9kIHRlbXBvciBpbmNpZGlkdW50IHV0IGxhYm9yZSBldCBkb2xvcmUgbWFn + bmEgYWxpcXVhLiBVdCBlbmltIGFkIG1pbmltIHZlbmlhbSwgcXVpcyBub3N0cnVkIGV4ZXJjaXRh + dGlvbiB1bGxhbWNvIGxhYm9yaXMgbmlzaSB1dCBhbGlxdWlwIGV4IGVhIGNvbW1vZG8gY29uc2Vx + dWF0LiBEdWlzIGF1dGUgaXJ1cmUgZG9sb3IgaW4gcmVwcmVoZW5kZXJpdCBpbiB2b2x1cHRhdGUg + dmVsaXQgZXNzZSBjaWxsdW0gZG9sb3JlIGV1IGZ1Z2lhdCBudWxsYSBwYXJpYXR1ci4gRXhjZXB0 + ZXVyIHNpbnQgb2NjYWVjYXQgY3VwaWRhdGF0IG5vbiBwcm9pZGVudCwgc3VudCBpbiBjdWxwYSBx + dWkgb2ZmaWNpYSBkZXNlcnVudCBtb2xsaXQgYW5pbSBpZCBlc3QgbGFib3J1bS4KTG9yZW0gaXBz + dW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2ljaW5nIGVsaXQsIHNlZCBkbyBl + aXVzbW9kIHRlbXBvciBpbmNpZGlkdW50IHV0IGxhYm9yZSBldCBkb2xvcmUgbWFnbmEgYWxpcXVh + LiBVdCBlbmltIGFkIG1pbmltIHZlbmlhbSwgcXVpcyBub3N0cnVkIGV4ZXJjaXRhdGlvbiB1bGxh + bWNvIGxhYm9yaXMgbmlzaSB1dCBhbGlxdWlwIGV4IGVhIGNvbW1vZG8gY29uc2VxdWF0LiBEdWlz + IGF1dGUgaXJ1cmUgZG9sb3IgaW4gcmVwcmVoZW5kZXJpdCBpbiB2b2x1cHRhdGUgdmVsaXQgZXNz + ZSBjaWxsdW0gZG9sb3JlIGV1IGZ1Z2lhdCBudWxsYSBwYXJpYXR1ci4gRXhjZXB0ZXVyIHNpbnQg + b2NjYWVjYXQgY3VwaWRhdGF0IG5vbiBwcm9pZGVudCwgc3VudCBpbiBjdWxwYSBxdWkgb2ZmaWNp + YSBkZXNlcnVudCBtb2xsaXQgYW5pbSBpZCBlc3QgbGFib3J1bS4KVGhlIGtleSBpcyAicGFydGlj + dWxhdGUiLgpMb3JlbSBpcHN1bSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzaWNp + bmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRv + bG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQg + ZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9k + byBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZv + bHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVy + LiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGlu + IGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpM + b3JlbSBpcHN1bSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzaWNpbmcgZWxpdCwg + c2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWdu + YSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0 + aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1 + YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2 + ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRl + dXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1 + aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLg== diff --git a/puzzles/net-re/200/key b/puzzles/net-re/200/key new file mode 100644 index 0000000..a22348b --- /dev/null +++ b/puzzles/net-re/200/key @@ -0,0 +1 @@ +particulate diff --git a/puzzles/net-re/2000/3k.pcap b/puzzles/net-re/2000/3k.pcap new file mode 100644 index 0000000..86b4922 Binary files /dev/null and b/puzzles/net-re/2000/3k.pcap differ diff --git a/puzzles/net-re/2000/index.mdwn b/puzzles/net-re/2000/index.mdwn new file mode 100644 index 0000000..d335971 --- /dev/null +++ b/puzzles/net-re/2000/index.mdwn @@ -0,0 +1,2 @@ +A Covert Channel +================ diff --git a/puzzles/net-re/2000/key b/puzzles/net-re/2000/key new file mode 100644 index 0000000..ecb1a5a --- /dev/null +++ b/puzzles/net-re/2000/key @@ -0,0 +1 @@ +obtuse diff --git a/puzzles/net-re/250/example.com-1.pcap b/puzzles/net-re/250/example.com-1.pcap new file mode 100644 index 0000000..e723d98 Binary files /dev/null and b/puzzles/net-re/250/example.com-1.pcap differ diff --git a/puzzles/net-re/250/hint b/puzzles/net-re/250/hint new file mode 100644 index 0000000..fde89a0 --- /dev/null +++ b/puzzles/net-re/250/hint @@ -0,0 +1,2 @@ +strings *pcap | grep alice +or use wireshark and pore through TCP diff --git a/puzzles/net-re/250/index.mdwn b/puzzles/net-re/250/index.mdwn new file mode 100644 index 0000000..6db305c --- /dev/null +++ b/puzzles/net-re/250/index.mdwn @@ -0,0 +1 @@ +What is Alice's email address? \ No newline at end of file diff --git a/puzzles/net-re/250/key b/puzzles/net-re/250/key new file mode 100644 index 0000000..38daa13 --- /dev/null +++ b/puzzles/net-re/250/key @@ -0,0 +1 @@ +alice_test@hotmail.com diff --git a/puzzles/net-re/25000/b143bbda4bbce4e81ac720a6bbc0d7c6 b/puzzles/net-re/25000/b143bbda4bbce4e81ac720a6bbc0d7c6 new file mode 100644 index 0000000..cc3e8f8 Binary files /dev/null and b/puzzles/net-re/25000/b143bbda4bbce4e81ac720a6bbc0d7c6 differ diff --git a/puzzles/net-re/25000/hint b/puzzles/net-re/25000/hint new file mode 100644 index 0000000..ba228fc --- /dev/null +++ b/puzzles/net-re/25000/hint @@ -0,0 +1 @@ +abandon hope diff --git a/puzzles/net-re/25000/key b/puzzles/net-re/25000/key new file mode 100644 index 0000000..84bd5ba --- /dev/null +++ b/puzzles/net-re/25000/key @@ -0,0 +1 @@ +galloping gallimimus diff --git a/puzzles/net-re/3/index.mdwn b/puzzles/net-re/3/index.mdwn new file mode 100644 index 0000000..27a6f83 --- /dev/null +++ b/puzzles/net-re/3/index.mdwn @@ -0,0 +1,125 @@ +Identifying Application Protocols +================================= + +The three easiest protocols to identify are FTP, SMTP, and HTTP. These +also happen to be some of the most common protocols in use. + +In these examples, lines either begin with `C:` (client) or `S:` +(server). + + +FTP (control channel only) +-------------------------- + + S: 220 ScumFTPD + C: USER anonymous + S: 331 Anonymous login ok, use email address as password + C: PASS joe@example.org + S: 230-Welcome to the FTP server. + S: 230 Anonymous access granted, restrictions apply. + C: PASV + S: 227 Entering Passive Mode (152,46,7,80,196,9). + C: LIST + S: 150 Opening ASCII mode data connection for file list + S: 226 Transfer complete + C: QUIT + S: 221 Goodbye + + +SMTP +---- + + S: 220 mail.example.com ESMTP MushMail 1.3 + C: EHLO bub + S: 250-Hi there + S: 250-VRFY + S: 250 8BITMIME + C: MAIL FROM: bob@example.com + S: 250 Recipient address accepted + C: RCPT TO: alice@example.com + S: 250 Sender accepted + C: DATA + S: 354 End data with \n.\n + C: From: Santa Claus + C: To: Alice + C: Subject: ho ho ho + C: + C: You've been a good girl this year, Alice. + C: . + S: 250 Message accepted for delivery + C: QUIT + S: 221 Goodbye + + +Note here that the `MAIL FROM` is different from the `From:` header +field. `MAIL FROM` and `RCPT TO` are called the β€œenvelope” and are what +the mail server looks at. The `From:` header field is merely advisory, +and can be trivially spoofed! + + +HTTP +---- + + C: GET /path/to/resource.html HTTP/1.1 + C: Host: www.example.com + C: User-Agent: Mozilla/2.0 (Galeon 1.0; Unicos; 2.3) + C: Connection: Close + C: + S: HTTP/1.1 200 OK + S: Server: CERN httpd 1.2 + S: Date: Fri, 22 May 2009 14:34:12 GMT + S: Last-Modified: Wed, 20 May 2009 10:33:42 GMT + S: Content-length: 20 + S: + S: hihi. + +The first line of an HTTP connection consists of: + + METHOD PATH VERSION + +`PATH` is the path to the resource being requested. It usually begins +with `/`, but if the client is trying to use the server as an HTTP +proxy, it will be a full URL. + +`VERSION` is the version of HTTP in use. It always begins with `HTTP/` +and ends with major and minor version numbers, separated by a period. + +`METHOD` is typically either `GET`, `HEAD`, or `POST`, but may also be +`OPTIONS`, `PUT`, `DELETE`, `TRACE`, `CONNECT`, or any number of +extensions. + +The `CONNECT` method is used to proxy traffic through the HTTP server. +Typically this is done by web browsers set up to use HTTP proxies for +HTTPS (HTTP over SSL), but is worth noting since it can also be used by +malware or to skirt firewall policies. For instance: + + CONNECT us.undernet.org:6667 HTTP/1.0 + +Would open an IRC connection to the Undernet IRC network. If your +policies disallow connecting to IRC, this demonstrates a possibly +successful attempt to skirt firewall rules. + + +Question +======== + +What follows is a list of the first line of text sent in various +different connections. The key for this page is the comma-separated (no +spaces) list of protocols not described on this page, ordered from +lowest (1) to highest (F). + + 1: GET / HTTP/1.1 + 2: +OK example.com server ready + 3: 220 mailrelay.example.com ESMTP Postfix 2.3.3/Bantu + 4: QUERY: //SYSTEMS/5B669A24 + 5: POST /depts/research/beekeeping/survey.php?token=83927400 HTTP/1.1 + 6: NICK rutabaga + 7: HEAD /content/images/ap-5823.jpg HTTP/1.0 + 8: -l jsmith + 9: CONNECT example.com:996 HTTP/1.1 + A: USER robot robot robot :robot + B: EHLO example.com + C: Subject: all-employee notice + D: * OK [CAPABILITY STARTTLS] example.com server Innova ready + E: TRACE / HTTP/1.1 + F: GET / ICAP/1.1 diff --git a/puzzles/net-re/3/key b/puzzles/net-re/3/key new file mode 100644 index 0000000..7acf87e --- /dev/null +++ b/puzzles/net-re/3/key @@ -0,0 +1 @@ +2,4,6,8,A,B,C,D,F diff --git a/puzzles/net-re/30/index.mdwn b/puzzles/net-re/30/index.mdwn new file mode 100644 index 0000000..9c6fbc3 --- /dev/null +++ b/puzzles/net-re/30/index.mdwn @@ -0,0 +1,3 @@ +What middle row is optimium for typing in "Moby Dick"? + +All caps, no spaces. diff --git a/puzzles/net-re/30/key b/puzzles/net-re/30/key new file mode 100644 index 0000000..747323e --- /dev/null +++ b/puzzles/net-re/30/key @@ -0,0 +1 @@ +RSTNFGEAID diff --git a/puzzles/net-re/30/session.pcap b/puzzles/net-re/30/session.pcap new file mode 100644 index 0000000..4d85ff6 Binary files /dev/null and b/puzzles/net-re/30/session.pcap differ diff --git a/puzzles/net-re/300/index.mdwn b/puzzles/net-re/300/index.mdwn new file mode 100644 index 0000000..126ae0d --- /dev/null +++ b/puzzles/net-re/300/index.mdwn @@ -0,0 +1,2 @@ +What sort of plants does this farm grow? + diff --git a/puzzles/net-re/300/key b/puzzles/net-re/300/key new file mode 100644 index 0000000..68fa8d8 --- /dev/null +++ b/puzzles/net-re/300/key @@ -0,0 +1 @@ +pumpkins diff --git a/puzzles/net-re/300/session.pcap b/puzzles/net-re/300/session.pcap new file mode 100644 index 0000000..b3eda4e Binary files /dev/null and b/puzzles/net-re/300/session.pcap differ diff --git a/puzzles/net-re/3000/12a18338d10d3a764f8d95e40b244cd2 b/puzzles/net-re/3000/12a18338d10d3a764f8d95e40b244cd2 new file mode 100644 index 0000000..cbbbd09 Binary files /dev/null and b/puzzles/net-re/3000/12a18338d10d3a764f8d95e40b244cd2 differ diff --git a/puzzles/net-re/3000/key b/puzzles/net-re/3000/key new file mode 100644 index 0000000..0d6431b --- /dev/null +++ b/puzzles/net-re/3000/key @@ -0,0 +1 @@ +galactic octopus diff --git a/puzzles/net-re/4/index.mdwn b/puzzles/net-re/4/index.mdwn new file mode 100644 index 0000000..4948b1e --- /dev/null +++ b/puzzles/net-re/4/index.mdwn @@ -0,0 +1,123 @@ +Multipurpose Internet Mail Extensions (MIME) +============================================ + +MIME is a standard to describe the type of content. It is used +extensively by HTTP and email clients to provide details about what sort +of thing is being transferred (for example: a JPEG image, a Zip file, an +HTML page). + +MIME is also used heavily by email clients to encapsulate multiple +objects, through the use of `multipart MIME`, more commonly referred to +as β€œattachments”. + +When examining an SMTP transaction, an analyst is frequently called upon +to β€œdecode” the MIME part in order to obtain the file that was +transferred. + +The following SMTP transaction features an attachment: + + S: 220 mail.example.com ESMTP MushMail 1.3 + C: EHLO bub + S: 250-Hi there + S: 250-VRFY + S: 250 8BITMIME + C: MAIL FROM: alice@example.com + S: 250 Recipient address accepted + C: RCPT TO: bob@example.com + S: 250 Sender accepted + C: DATA + S: 354 End data with \n.\n + C: From: Alice + C: To: Bob + C: Subject: TPS report + C: MIME-Version: 1.0 + C: Content-Type: multipart/mixed; boundary=arf + C: + C: This is a MIME message. Apparently your software is ancient + C: and is unable to render it properly. Too bad for you. + C: + C: --arf + C: Content-type: text/plain + C: Content-disposition: inline + C: + C: I've attached the TPS report you asked for. + C: --arf + C: Content-type: image/png + C: Content-transfer-encoding: base64 + C: Content-disposition: attachment; filename=key.png + C: + C: iVBORw0KGgoAAAANSUhEUgAAAHEAAAALCAIAAADHpfUgAAAACXBIWXMAAAsT + C: AAALEwEAmpwYAAAAB3RJTUUH2gEOFzovNd+dvwAAAB10RVh0Q29tbWVudABD + C: cmVhdGVkIHdpdGggVGhlIEdJTVDvZCVuAAAAz0lEQVRIx+1Wyw7EIAiUDf// + C: y+6hCTEO4NhqN5s4h8YaGB6CKLXWcrAUn5OC5dBSiojYv5WtiFxrW9ja3WlJ + C: kSSygjCxVibiiWwhD2pFUUdaSTY6Zs09SzY7MHdIbsX1OCLJXUIeV2tYK4zP + C: GDvV+3gaDNxj5JGXA2/r/YGhfIStx+i927O/Quvt7D3D1ErOozy762ikeO3b + C: 93aiGR5XZqpnExkMcBi77iuuaKrs4Olknpzi86tDV2WQGevDbojG8abeX6KF + C: sct58583/x/gCxug/wCTSHakAAAAAElFTkSuQmCC + C: --arf-- + C: . + S: 250 Message accepted for delivery + C: QUIT + S: 221 Goodbye + +The attachment part of this can be easily spotted: it’s the large +Base64-encoded chunk in the bottom half. You can spot the type +(image/png) and the filename (domo.png) in the MIME headers immediately +preceding the block. + +The Base64 text can be copied and pasted into a text editor for +decoding. Save the text to any file you want: this tutorial will use +the filename `key.png.txt`. + + +Easily Decoding Base64 +====================== + +Most Unix systems come pre-installed with several programs that can +decode Base64: uudecode, openssl, perl, and python are all capable of +the task. We will demonstrate Python, since we will be using that +language later in this tutorial, and since it is available on Windows +also. + +After starting Python, we are met with the Python prompt: + + >>> + +We now open the file and read in its contents: + + >>> contents = open('key.png.txt').read() + +The file’s contents are now in the `contents` variable. We can then +Base64 decode the contents: + + >>> import binascii + >>> decode = binascii.a2b_base64(contents) + +And save the decoded contents to a new file, called `key.png`: + + >>> open('key.png', 'wb').write(decode) + +If you are confused by the syntax, don’t worry too much about it. You +can use these four lines as a boilerplate for base64 decoding any file. + + +Some help from Unix +=================== + +Unix (or Cygwin on Windows) features a command called `file` which +encapsulates decades of knowledge about file formats. The `file` +command can be run on arbitrary data to get an initial idea about what +sort of file you have. In our example: + + $ file key.png + key.png: PNG image data, 113 x 11, 8-bit/color RGB, non-interlaced + +This tool is invaluable when analyzing unknown data. + + +Question +======== + +Use the techniques in this page to decode the Base64 attachment used in +the example. When properly decoded, you will have an image that, when +viewed, reveals the key for this page. diff --git a/puzzles/net-re/4/key b/puzzles/net-re/4/key new file mode 100644 index 0000000..15243ae --- /dev/null +++ b/puzzles/net-re/4/key @@ -0,0 +1 @@ +PINHEAD CATASTROPHE diff --git a/puzzles/net-re/400/index.mdwn b/puzzles/net-re/400/index.mdwn new file mode 100644 index 0000000..43eeca4 --- /dev/null +++ b/puzzles/net-re/400/index.mdwn @@ -0,0 +1 @@ +What is Domo in now? diff --git a/puzzles/net-re/400/key b/puzzles/net-re/400/key new file mode 100644 index 0000000..84fa0b7 --- /dev/null +++ b/puzzles/net-re/400/key @@ -0,0 +1 @@ +lettuce diff --git a/puzzles/net-re/400/session.pcap b/puzzles/net-re/400/session.pcap new file mode 100644 index 0000000..72e7884 Binary files /dev/null and b/puzzles/net-re/400/session.pcap differ diff --git a/puzzles/net-re/4000/e464f161483b4a2fd8b690a98932b703 b/puzzles/net-re/4000/e464f161483b4a2fd8b690a98932b703 new file mode 100644 index 0000000..5166f67 Binary files /dev/null and b/puzzles/net-re/4000/e464f161483b4a2fd8b690a98932b703 differ diff --git a/puzzles/net-re/4000/key b/puzzles/net-re/4000/key new file mode 100644 index 0000000..f9982e8 --- /dev/null +++ b/puzzles/net-re/4000/key @@ -0,0 +1 @@ +gaucho moped fleet diff --git a/puzzles/net-re/5/example1.pcap b/puzzles/net-re/5/example1.pcap new file mode 100644 index 0000000..ca71898 Binary files /dev/null and b/puzzles/net-re/5/example1.pcap differ diff --git a/puzzles/net-re/5/index.mdwn b/puzzles/net-re/5/index.mdwn new file mode 100644 index 0000000..2162973 --- /dev/null +++ b/puzzles/net-re/5/index.mdwn @@ -0,0 +1,92 @@ +PCAP files +========== + +When packets are captured off of a network, they are typically written +to PCAP (Packet CAPture) files. These files contain some information +about the device used to do the capture, and a list of packets that were +captured along with what precise time they were captured. + +PCAP files are invaluable in network reverse engineering. Without +packet capture, all you can usually do is speculate. It's like the +difference between looking at clues at the scene of the crime, and +having a videotape from cameras providing video at every angle. + +With full packet capture (capture of all inbound and outbound traffic to +a network), it is sometimes even possible to write *protocol decoders*, +which can be used to show exactly what information went back and forth, +at what times. This ability is of extremely high value in any sort of +forensic investigation. + + +Wireshark +--------- + +Wireshark is a graphical PCAP viewing tool for Unix, Windows, or MacOS. +It features built-in protocol decoders for many standard protocols, +powerful filters for examining captures, statistical tools, and much +more. It is capable of capturing packets on its own, and reading and +writing PCAP files. + +Let’s load up Wireshark. + +![Wireshark’s Startup](ws-start.png) + +We can now open [an example pcap file](example1.pcap) with β€œFile -> +Open”. After loading, we will see Wireshark’s 3-panel display: + +![Loaded file](ws-opened.png) + +The top panel shows a list of frames (packets). The middle panel shows +information about the currently-selected frame, and the bottom panel +shows a hex dump of the frame. We can click on one of the white +triangles to expand information about an aspect of the frame, and by +highlighting any line in the second panel, the third panel will +highlight that part of the hex dump: + +![Examining a frame](ws-examine.png) + +We can also right-click on a frame in the top panel for a drop-down menu +of options. We will focus on the β€œfollow TCP stream” option. + +![Follow a stream](ws-follow.png) + +This opens up a new window in which both sides of the TCP stream are +reassembled from the individual frames: + +![Followed stream](ws-stream.png) + +This text can be copied and pasted into a text editor or a binary +editor as a means to quickly extract payloads like attachments or +transferred files. + + +Wireshark’s weakness, and `tcpflow` +----------------------------------- + +Wireshark’s TCP reassembly routines have bugs. Sometimes, stream +reassembly is incomplete, even though all the frames are present. + +Under Unix (or Cygwin in Windows), a program called `tcpflow` can do +much more reliable--not to mention quick--reassembly of TCP sessions: + + $ ls + example1.pcap + $ tcpflow -r example1.pcap + $ ls + 106.086.094.175.06063-239.201.176.004.65167 + 239.201.176.004.65167-106.086.094.175.06063 + example1.pcap + $ + +`tcpflow` reassembles each side of a connection, which is usually all +you want anyway. This has reassembled into the β€œ106.86.94.175 side” and +the β€œ239.201.176.4 side”; each file contains what the left IP sent to +the right IP. + + +Question +======== + +Use Wireshark or `tcpflow` to extract the TCP stream from [this packet +capture](key.pcap). Then, using techniques from previous sections, +decode the payload. View it to find the key for this page. diff --git a/puzzles/net-re/5/key b/puzzles/net-re/5/key new file mode 100644 index 0000000..2dfaad7 --- /dev/null +++ b/puzzles/net-re/5/key @@ -0,0 +1 @@ +fishsticks diff --git a/puzzles/net-re/5/key.pcap b/puzzles/net-re/5/key.pcap new file mode 100644 index 0000000..c3305df Binary files /dev/null and b/puzzles/net-re/5/key.pcap differ diff --git a/puzzles/net-re/5/ws-examine.png b/puzzles/net-re/5/ws-examine.png new file mode 100644 index 0000000..d5a7c04 Binary files /dev/null and b/puzzles/net-re/5/ws-examine.png differ diff --git a/puzzles/net-re/5/ws-follow.png b/puzzles/net-re/5/ws-follow.png new file mode 100644 index 0000000..2e0f309 Binary files /dev/null and b/puzzles/net-re/5/ws-follow.png differ diff --git a/puzzles/net-re/5/ws-opened.png b/puzzles/net-re/5/ws-opened.png new file mode 100644 index 0000000..64c777f Binary files /dev/null and b/puzzles/net-re/5/ws-opened.png differ diff --git a/puzzles/net-re/5/ws-start.png b/puzzles/net-re/5/ws-start.png new file mode 100644 index 0000000..9674794 Binary files /dev/null and b/puzzles/net-re/5/ws-start.png differ diff --git a/puzzles/net-re/5/ws-stream.png b/puzzles/net-re/5/ws-stream.png new file mode 100644 index 0000000..a2fec96 Binary files /dev/null and b/puzzles/net-re/5/ws-stream.png differ diff --git a/puzzles/net-re/5000/53127bca7fed4875920675c2c9e14597 b/puzzles/net-re/5000/53127bca7fed4875920675c2c9e14597 new file mode 100644 index 0000000..0afde20 Binary files /dev/null and b/puzzles/net-re/5000/53127bca7fed4875920675c2c9e14597 differ diff --git a/puzzles/net-re/5000/key b/puzzles/net-re/5000/key new file mode 100644 index 0000000..971b5d9 --- /dev/null +++ b/puzzles/net-re/5000/key @@ -0,0 +1 @@ +miniature commodore exercise diff --git a/puzzles/net-re/6/index.mdwn b/puzzles/net-re/6/index.mdwn new file mode 100644 index 0000000..24531b9 --- /dev/null +++ b/puzzles/net-re/6/index.mdwn @@ -0,0 +1,87 @@ +More Application Protocols +========================== + +Several popular application-layer protocols are what's referred to as +β€œbinary protocols”, meaning their communications are not easily readable +by humans. They are still readable, though, if you know how to read +them. + + +Secure Shell (SSH) +------------------ + +Although SSH is a binary protocol, the first message sent by both client +and server is typically an ASCII banner announcing the version: + + S: SSH-1.99-OpenSSH_5.1p1 Debian-5 + C: SSH-2.0-OpenSSH_3.9p1 + +In SSH, the server speaks first. + + + +Transport Layer Security (TLS) +------------------------------ + +TLS (formerly known as Secure Sockets Layer or SSL) is a protocol for +encrypting communications over TCP. While the contents of an SSL +session are encrypted, we can at least identify it as such by looking at +the beginning few bytes: + +
+
Hex
+
C: 16 03 01 00 8a 01 00 01 00 00 86 03 +01…
+S: 16 03 01 00 4a 02 00 00 46 03 +01…
+
ASCII
+
C: ^V^C^A^@\e212^A^@^A^@^@\e206^C^A…
+S: ^V^C^A^@\e112^B^@^@\e106^C^A…
+
+ +The primary indicator of SSL is that both sides of the conversation send +hex value 0x16 (`^V`) as their first byte. A secondary indicator is a +repeated version number (0x03 0x01). + +In TLS, the client speaks first. + + + +Domain Name Service (DNS) +------------------------- + +DNS, a very frequently-occuring protocol, can be identified by the +combination of its port (53) and its typical payload. Examples follow: + +
+
Hex
+
3a fb 01 00 00 01 00 00 00 00 00 00 07 65 78 61 6d 70 6c +65 03 63 6f 6d 00
+
ASCII
+
H\e373^A^@^@^A^@^@^@^@^@^@^Gexample^Ccom^A^@ +
+ +The first highlighted part, with the *opcode* (1, an β€œA record request”, +which asks for an IP given a name), and the name to be resolved +(example.com). + +The name is encoded by preceding it by a byte count, and then than many +bytes. Breaking this name apart, we can see: + + \x07 β€œexample” + \x03 β€œcom” + \x00 + +The protocol is decoded by first reading in the length, then that number +of bytes. This continues until a length of 0 is encountered. This +method of encoding strings is very common in binary protocols. + + + +Question +======== + +The following is an extract of a DNS packet. What does this decode to? + + 087768617465766572076578616d706c65036e657400 + diff --git a/puzzles/net-re/6/key b/puzzles/net-re/6/key new file mode 100644 index 0000000..86e6494 --- /dev/null +++ b/puzzles/net-re/6/key @@ -0,0 +1 @@ +whatever.example.net diff --git a/puzzles/net-re/7/index.mdwn b/puzzles/net-re/7/index.mdwn new file mode 100644 index 0000000..3ee8d17 --- /dev/null +++ b/puzzles/net-re/7/index.mdwn @@ -0,0 +1,210 @@ +More Binary Protocols +===================== + +The previous page introduced you to decoding binary protocols by showing +how DNS encodes text. We will now examine Ethernet, IP, and TCP, to +better understand binary protocols. + +Generally, you will use tools like wireshark or tcpdump to decode IP and +TCP, but understanding how these work will help you decode other unknown +binary protocols and file formats in the future. + +In this page, we will dissect the following captured network frame: + + 00 11 bc 56 5f 00 00 01 e8 13 0c 89 08 00 45 00 + 00 3c 13 b3 36 3a 3f 06 13 be 6a 56 5e af ef c9 + b0 04 17 af 00 50 cf 16 e8 db 00 00 00 00 a0 02 + 16 d0 ec 87 00 00 02 04 05 b4 04 02 08 0a 93 40 + fb 68 00 00 00 00 01 03 03 07 + + +Octets +------ + +This page introduces the new term β€œoctet” to refer to 8 bits. The words +β€œoctet” and β€œbyte” are usually interchangeable, but in some cases a byte +may be 7 bits, 6 bits, 16 bits, or something else. Networking documents +usually refer to β€œoctets” in order to avoid any confusion about the +number of bits. + + +Ethernet +-------- + +Ethernet consists of: + +* Destination address (6 octets) +* Source address (6 octets) +* EtherType (2 octets) +* Data +* CRC Checksum (4 octets) + +Let’s examine the first 14 octets of our captured ethernet frame: + + 00 11 bc 56 5f 00 00 01 e8 13 0c 89 08 00 + +This can be broken down into the two 6-octet addresses and the EtherType: + + dst: 00:11:bc:56:5f:00 + src: 00:01:e8:13:0c:89 + typ: 0800 + +The destination and source should be recognizable as β€œMAC addresses”. +An EtherType of 0x0800 indicates the data is an IPv4 packet. Most +frames you encounter will have an EtherType of 0x0800. + + +Internet Protocol (IP version 4) +-------------------------------- + +IP introduces to us the notion of 4-bit and 13-bit integers. The +following chart from RFC 971 shows the fields of an IP header. The +numbers at the top are the *bit* offset of each field. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |Version| IHL |Type of Service| Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification |Flags| Fragment Offset | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Time to Live | Protocol | Header Checksum | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Source Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Destination Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options | Padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +The next few octets of our network frame are: + + __ __ __ __ __ __ __ __ __ __ __ __ __ __ 45 00 + 00 3c 13 b3 36 3a 3f 06 13 be 6a 56 5e af ef c9 + b0 04 17 af 00 50 cf 16 e8 db 00 00 + +Let’s begin to break this down. The first two fields are 4-bit +integers: half-octets, also called nybbles (because they are half a +byte). These are easy to extract from hex, since one hex digit is +exactly 4 bits. Similarly, one octet is 8 bits, and two hex digits. +Two octets is 16 bits, and four hex digits. + + Version: 0x4 + IHL: 0x4 + TOS: 0x00 + Length: 0x003c + ID: 0x13b3 + +Leaving our undecoded snippet as follows: + + __ __ __ __ 36 3a 3f 06 13 be 6a 56 5e af ef c9 + b0 04 17 af 00 50 cf 16 e8 db 00 00 + +The next field, β€œFlags”, is a *3-bit* field, leaving 13 bits for +β€œFragment Offset”. Let’s look at the next 16 bits: + + 3 6 3 A + 0011 0110 0011 1010 + +Now let’s split that up after the first three bits: + + 1 1 6 3 A + 001 1 0110 0011 1010 + +Therefore, our first three bits are the hex value bits are the hex value +`0x1`, and the remaining 16 bits are the hex value `0x163A`. + + Flags: 0x1 + Fragment Offset: 0x163A + +The next few fields are relatively simple: + + TTL: 0x3F + Protocol: 0x06 + Header Checksum: 0x13BE + +Now our snippet is: + + __ __ __ __ __ __ __ __ __ __ 6a 56 5e af ef c9 + b0 04 17 af 00 50 cf 16 e8 db 00 00 + +Finally, we get to the IP addresses: + + Src: 6a.56.5e.af (106.86.94.175) + Dst: ef.c9.b0.04 (239.201.176.4) + +We have now decoded 20 octets, or 4 words. The IHL field contains the +length of the IP header in words. Since IHL in this frame is 4, this IP +header has no options, and we are done decoding it. All remaining data +belongs to the TCP protocol (protocol 6). + + +Transmission Control Protocol (TCP) +----------------------------------- + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Source Port | Destination Port | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Acknowledgment Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data | |U|A|P|R|S|F| | + | Offset| Reserved |R|C|S|S|Y|I| Window | + | | |G|K|H|T|N|N| | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Checksum | Urgent Pointer | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Options | Padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +After parsing the Ethernet and IP headers, our frame’s unparsed octets +are: + + __ __ 17 af 00 50 cf 16 e8 db 00 00 00 00 a0 02 + 16 d0 ec 87 00 00 02 04 05 b4 04 02 08 0a 93 40 + fb 68 00 00 00 00 01 03 03 07 + +The first 4 fields are decoded just like previous fields: + + Src: 0x17AF (6063) + Dst: 0x0050 (80) + Seq: 0xcf16e8db + Ack: 0x00000000 + +We can now examine the next 2 octets to extract Data Offset and the +flags: + + a 0 0 2 + 1000 0000 0000 0010 + offs|reserv UA PRSF + RC SSYI + GK HTNN + +Decoding to: + + Data Offset: 0xA + Flags: SYN + +Then, carrying on: + + Window: 0x16D0 + Checksum: 0xEC87 + Urgent: 0x0000 + +There follows 20 bytes of options, and no data. + + +Question +======== + +The key for this page is the source IP in the following frame: + + 00 11 bc 56 5f 00 00 01 e8 13 0c 89 08 00 45 00 + 00 2c 00 00 40 00 3d 06 aa 51 3c 00 0d 41 4d 01 + 08 30 c6 d6 d2 e6 06 57 7b 2d 00 76 d2 c9 60 12 + 16 d0 84 c5 00 00 02 04 05 b4 00 00 diff --git a/puzzles/net-re/7/key b/puzzles/net-re/7/key new file mode 100644 index 0000000..c6faf04 --- /dev/null +++ b/puzzles/net-re/7/key @@ -0,0 +1 @@ +60.0.13.65 diff --git a/puzzles/net-re/700/index.mdwn b/puzzles/net-re/700/index.mdwn new file mode 100644 index 0000000..02b218f --- /dev/null +++ b/puzzles/net-re/700/index.mdwn @@ -0,0 +1,32 @@ + ZkVYT0cKQ1pZX0cKTkVGRVgKWUNeCktHT14GCklFRFlPSV5PXl9YCktOQ1pDWUNJQ0RNCk9GQ14G + CllPTgpORQpPQ19ZR0VOCl5PR1pFWApDRElDTkNOX0ReCl9eCkZLSEVYTwpPXgpORUZFWE8KR0tN + REsKS0ZDW19LBAp/XgpPRENHCktOCkdDRENHClxPRENLRwYKW19DWQpERVleWF9OCk9ST1hJQ15L + XkNFRApfRkZLR0lFCkZLSEVYQ1kKRENZQwpfXgpLRkNbX0NaCk9SCk9LCklFR0dFTkUKSUVEWU9b + X0teBApuX0NZCktfXk8KQ1hfWE8KTkVGRVgKQ0QKWE9aWE9CT0ROT1hDXgpDRApcRUZfWl5LXk8K + XE9GQ14KT1lZTwpJQ0ZGX0cKTkVGRVhPCk9fCkxfTUNLXgpEX0ZGSwpaS1hDS15fWAQKb1JJT1pe + T19YCllDRF4KRUlJS09JS14KSV9aQ05LXkteCkRFRApaWEVDTk9EXgYKWV9EXgpDRApJX0ZaSwpb + X0MKRUxMQ0lDSwpOT1lPWF9EXgpHRUZGQ14KS0RDRwpDTgpPWV4KRktIRVhfRwQgZkVYT0cKQ1pZ + X0cKTkVGRVgKWUNeCktHT14GCklFRFlPSV5PXl9YCktOQ1pDWUNJQ0RNCk9GQ14GCllPTgpORQpP + Q19ZR0VOCl5PR1pFWApDRElDTkNOX0ReCl9eCkZLSEVYTwpPXgpORUZFWE8KR0tNREsKS0ZDW19L + BAp/XgpPRENHCktOCkdDRENHClxPRENLRwYKW19DWQpERVleWF9OCk9ST1hJQ15LXkNFRApfRkZL + R0lFCkZLSEVYQ1kKRENZQwpfXgpLRkNbX0NaCk9SCk9LCklFR0dFTkUKSUVEWU9bX0teBApuX0NZ + CktfXk8KQ1hfWE8KTkVGRVgKQ0QKWE9aWE9CT0ROT1hDXgpDRApcRUZfWl5LXk8KXE9GQ14KT1lZ + TwpJQ0ZGX0cKTkVGRVhPCk9fCkxfTUNLXgpEX0ZGSwpaS1hDS15fWAQKb1JJT1peT19YCllDRF4K + RUlJS09JS14KSV9aQ05LXkteCkRFRApaWEVDTk9EXgYKWV9EXgpDRApJX0ZaSwpbX0MKRUxMQ0lD + SwpOT1lPWF9EXgpHRUZGQ14KS0RDRwpDTgpPWV4KRktIRVhfRwQgfkJPCkFPUwpDWQoITENSS15P + ClhLWV5PWENQTwgEIGZFWE9HCkNaWV9HCk5FRkVYCllDXgpLR09eBgpJRURZT0leT15fWApLTkNa + Q1lDSUNETQpPRkNeBgpZT04KTkUKT0NfWUdFTgpeT0daRVgKQ0RJQ05DTl9EXgpfXgpGS0hFWE8K + T14KTkVGRVhPCkdLTURLCktGQ1tfSwQKf14KT0RDRwpLTgpHQ0RDRwpcT0RDS0cGCltfQ1kKREVZ + XlhfTgpPUk9YSUNeS15DRUQKX0ZGS0dJRQpGS0hFWENZCkRDWUMKX14KS0ZDW19DWgpPUgpPSwpJ + RUdHRU5FCklFRFlPW19LXgQKbl9DWQpLX15PCkNYX1hPCk5FRkVYCkNEClhPWlhPQk9ETk9YQ14K + Q0QKXEVGX1peS15PClxPRkNeCk9ZWU8KSUNGRl9HCk5FRkVYTwpPXwpMX01DS14KRF9GRksKWktY + Q0teX1gECm9SSU9aXk9fWApZQ0ReCkVJSUtPSUteCklfWkNOS15LXgpERUQKWlhFQ05PRF4GCllf + RF4KQ0QKSV9GWksKW19DCkVMTENJQ0sKTk9ZT1hfRF4KR0VGRkNeCktEQ0cKQ04KT1leCkZLSEVY + X0cEIGZFWE9HCkNaWV9HCk5FRkVYCllDXgpLR09eBgpJRURZT0leT15fWApLTkNaQ1lDSUNETQpP + RkNeBgpZT04KTkUKT0NfWUdFTgpeT0daRVgKQ0RJQ05DTl9EXgpfXgpGS0hFWE8KT14KTkVGRVhP + CkdLTURLCktGQ1tfSwQKf14KT0RDRwpLTgpHQ0RDRwpcT0RDS0cGCltfQ1kKREVZXlhfTgpPUk9Y + SUNeS15DRUQKX0ZGS0dJRQpGS0hFWENZCkRDWUMKX14KS0ZDW19DWgpPUgpPSwpJRUdHRU5FCklF + RFlPW19LXgQKbl9DWQpLX15PCkNYX1hPCk5FRkVYCkNEClhPWlhPQk9ETk9YQ14KQ0QKXEVGX1pe + S15PClxPRkNeCk9ZWU8KSUNGRl9HCk5FRkVYTwpPXwpMX01DS14KRF9GRksKWktYQ0teX1gECm9S + SU9aXk9fWApZQ0ReCkVJSUtPSUteCklfWkNOS15LXgpERUQKWlhFQ05PRF4GCllfRF4KQ0QKSV9G + WksKW19DCkVMTENJQ0sKTk9ZT1hfRF4KR0VGRkNeCktEQ0cKQ04KT1leCkZLSEVYX0cE diff --git a/puzzles/net-re/700/key b/puzzles/net-re/700/key new file mode 100644 index 0000000..06796b9 --- /dev/null +++ b/puzzles/net-re/700/key @@ -0,0 +1 @@ +fixate rasterize diff --git a/puzzles/net-re/8/index.mdwn b/puzzles/net-re/8/index.mdwn new file mode 100644 index 0000000..4444336 --- /dev/null +++ b/puzzles/net-re/8/index.mdwn @@ -0,0 +1,172 @@ +XOR Masks +========= + +Exclusive or (XOR, βŠ•) is the binary operation β€œone or the other but not +both”. The following table demonstrates how a binary XOR works: + + p q pβŠ•q + --------- + 0 0 0 + 0 1 1 + 1 0 1 + 1 1 0 + +To XOR two multi-bit numbers, one must simply XOR each bit individually: + + 11110000 + βŠ• 10101010 + ========== + 01011010 + + +Reversing XOR +------------- + +XOR has the peculiar property that `(pβŠ•q)βŠ•q = p` for any value of `p` or `q`: + + p q pβŠ•q (pβŠ•q)βŠ•q + ------------------ + 0 0 0 0 + 0 1 1 0 + 1 0 1 1 + 1 1 0 1 + +This also works for multi-bit numbers: + + 11110000 (0xF0) + βŠ• 10101010 (0xAA) + ========== + 01011010 (0x9A) + + 01011010 (0x9A) + βŠ• 10101010 (0xAA) + ========== + 11110000 (0xF0) + + +XOR in encryption +----------------- + +XOR is used extensively in many encryption algorithms. One reason it is +popular is because it is easy to implement in hardware, since there is +no possibility for overflow or underflow, there are no β€œcarry” bits as +in addition, and XOR is one of the basic logic gates used in +electronics. For these reasons, it is also one of the quickest +operations most CPUs can carry out. + +One of the most basic ways to use XOR in encryption is to XOR the +plaintext (the thing to be encrypted) against the key: + + ATTACK AT DAWN + βŠ• keykeykeykeyke + ================ + *1-*&2K$-K!8<+ + +Because of the reversible nature of XOR, the same key can be applied to +decrypt the ciphertext: + + *1-*&2K$-K!8<+ + βŠ• keykeykeykeyke + ================ + ATTACK AT DAWN + + +Doing XOR on strings in Python +------------------------------ + +The following function in Python 3: + + def xor(n, b): + return bytes(c ^ n for c in b) + +Will take a byte array, and return it XORed with n. + +Let's try an example: + + >>> def xor(n, b): + ... return bytes(c ^ n for c in b) + ... + >>> xor(22, b'hello') + b'~szzy' + >>> xor(22, b'~szzy') + b'hello' + >>> xor(22, bytes([0, 1, 2, 3, 4])) + b'\x16\x17\x14\x15\x12' + >>> + +We will use this last method of invoking xor in the following sections, +to work with hex dumps. + + +Converting hex strings to byte arrays +------------------------------------- + +The following Python 3 function will take an ASCII representation of hex +octets, and convert it to a byte array: + + >>> def unhex(s): + ... import binascii + ... return binascii.unhexlify(s.replace(' ', '').replace('\n', '')) + + +Known-plaintext attacks against XOR encryption +---------------------------------------------- + +We have intercepted a coded message. We suspect the plaintext to +consist solely of ASCII characters. The hex dump of the message is: + + + 00000000 69 62 65 0a 7d 6f 0a 78 0a 79 7f 61 0a 6c 63 72 ┆ibeβ—™}oβ—™xβ—™yβŒ‚aβ—™lcr┆ + 00000010 0a 67 19 0a 6d 1a 6b 7e 70 ┆◙g↓◙mβ†’k~p┆ + 00000019 + +Right away we can see that the character β—™ (`0x0A`) occurs fairly +frequently. We will first guess that `0x0A` represents the letter β€œe”, +which is the most common letter in English. To find the XOR key needed +to turn turn β€œe” (`0x65`) into `0x0A`, we can simply XOR the two values: + + 0x65 (e) + βŠ• 0x0A (β—™) + ====== + 0x6F (o) + +Let’s try applying the XOR key `0x6F` to the ciphertext. First, we will +load the hex octets into a Python 3 byte array using our `unhex` +function from the previous section: + + >>> a = unhex('69 62 65 0a 7d 6f 0a 78 0a 79 7f 61 0a 6c 63 72') + \ + ... unhex('0a 67 19 0a 6d 1a 6b 7e 70') + >>> a + b'ibe\n}o\nx\ny\x7fa\nlcr\ng\x19\nm\x1ak~p' + +Now, we'll xor it with our guess of `0x6F`: + >>> xor(0x6F, a) + b'\x06\r\ne\x12\x00e\x17e\x16\x10\x0ee\x03\x0c\x1de\x08ve\x02u\x04\x11\x1f' + +That doesn't look right. Let's try another guess, maybe β—™ represents a space: + + 0x20 ( ) + βŠ• 0x0A (β—™) + ====== + 0x2A (*) + +Now we apply this key: + + >>> xor(0x2A, a) + b'CHO WE R SUK FIX M3 G0ATZ' + +This is clearly English text, although possibly some sort of code. In +any case, we have broken the code with the key `0x2A`. + + +Question +======== + +Use the known-plaintext attack technique against this XORed cyphertext: + + 00000000 01 2d 2c 25 30 23 36 37 2e 23 36 2b 2d 2c 31 62 β”†β˜Ί-,%0#67.#6+-,1b┆ + 00000010 2d 2c 62 3b 2d 37 30 62 20 30 27 23 29 6c 62 62 ┆-,b;-70b␣0'#)lbb┆ + 00000020 16 2a 27 62 29 27 3b 62 24 2d 30 62 36 2a 2b 31 ┆▬*'b)';b$-0b6*+1┆ + 00000030 62 32 23 25 27 62 2b 31 62 60 20 23 21 2d 2c 60 ┆b2#%'b+1b`␣#!-,`┆ + 00000040 6c ┆l┆ + 00000041 diff --git a/puzzles/net-re/8/key b/puzzles/net-re/8/key new file mode 100644 index 0000000..6e953b2 --- /dev/null +++ b/puzzles/net-re/8/key @@ -0,0 +1 @@ +bacon diff --git a/puzzles/net-re/800/index.mdwn b/puzzles/net-re/800/index.mdwn new file mode 100644 index 0000000..14e85d8 --- /dev/null +++ b/puzzles/net-re/800/index.mdwn @@ -0,0 +1,26 @@ +What is the IP address of `bungle.dirtbags.net`? + + 18:59:44.128730 IP 67.18.176.203.50495 > 67.18.176.203.domain + 0x0000: 0000 0000 0000 0000 0000 0000 0800 4500 ..............E. + 0x0010: 0041 15e8 4000 4011 3d09 4312 b0cb 4312 .A..@.@.=.C...C. + 0x0020: b0cb c53f 0035 002d ebb4 11d4 0100 0001 ...?.5.-........ + 0x0030: 0000 0000 0000 0662 756e 676c 6508 6469 .......bungle.di + 0x0040: 7274 6261 6773 036e 6574 0000 0100 01 rtbags.net..... + 18:59:44.128786 IP 67.18.176.203.domain > 67.18.176.203.50495 + 0x0000: 0000 0000 0000 0000 0000 0000 0800 4500 ..............E. + 0x0010: 00fb 0000 4000 4011 5237 4312 b0cb 4312 ....@.@.R7C...C. + 0x0020: b0cb 0035 c53f 00e7 2b3b 11d4 8500 0001 ...5.?..+;...... + 0x0030: 0001 0005 0005 0662 756e 676c 6508 6469 .......bungle.di + 0x0040: 7274 6261 6773 036e 6574 0000 0100 01c0 rtbags.net...... + 0x0050: 0c00 0100 0100 000e 1000 040a 4894 42c0 ............H.B. + 0x0060: 1300 0200 0100 000e 1000 0603 6e73 31c0 ............ns1. + 0x0070: 13c0 1300 0200 0100 000e 1000 0603 6e73 ..............ns + 0x0080: 32c0 13c0 1300 0200 0100 000e 1000 0603 2............... + 0x0090: 6e73 33c0 13c0 1300 0200 0100 000e 1000 ns3............. + 0x00a0: 0603 6e73 34c0 13c0 1300 0200 0100 000e ..ns4........... + 0x00b0: 1000 0603 6e73 35c0 13c0 4100 0100 0100 ....ns5...A..... + 0x00c0: 000e 1000 0443 12b0 cbc0 5300 0100 0100 .....C....S..... + 0x00d0: 000e 1000 0445 5d7f 0ac0 6500 0100 0100 .....E]...e..... + 0x00e0: 000e 1000 0441 13b2 0ac0 7700 0100 0100 .....A....w..... + 0x00f0: 000e 1000 044b 7f60 0ac0 8900 0100 0100 .....K.`........ + 0x0100: 000e 1000 04cf c046 0a .......F. diff --git a/puzzles/net-re/800/key b/puzzles/net-re/800/key new file mode 100644 index 0000000..1724b8b --- /dev/null +++ b/puzzles/net-re/800/key @@ -0,0 +1 @@ +10.72.148.66 diff --git a/puzzles/sequence/1/index.mdwn b/puzzles/sequence/1/index.mdwn new file mode 100644 index 0000000..dd0e891 --- /dev/null +++ b/puzzles/sequence/1/index.mdwn @@ -0,0 +1 @@ + 1 2 3 4 5 _ diff --git a/puzzles/sequence/1/key b/puzzles/sequence/1/key new file mode 100644 index 0000000..1e8b314 --- /dev/null +++ b/puzzles/sequence/1/key @@ -0,0 +1 @@ +6 diff --git a/puzzles/sequence/100/index.mdwn b/puzzles/sequence/100/index.mdwn new file mode 100644 index 0000000..fa4d240 --- /dev/null +++ b/puzzles/sequence/100/index.mdwn @@ -0,0 +1 @@ + 21 22 25 2a 31 3a _ diff --git a/puzzles/sequence/100/key b/puzzles/sequence/100/key new file mode 100644 index 0000000..ea90ee3 --- /dev/null +++ b/puzzles/sequence/100/key @@ -0,0 +1 @@ +45 diff --git a/puzzles/sequence/16/index.mdwn b/puzzles/sequence/16/index.mdwn new file mode 100644 index 0000000..d2caefa --- /dev/null +++ b/puzzles/sequence/16/index.mdwn @@ -0,0 +1 @@ + 5 6 7 8 9 _ diff --git a/puzzles/sequence/16/key b/puzzles/sequence/16/key new file mode 100644 index 0000000..7898192 --- /dev/null +++ b/puzzles/sequence/16/key @@ -0,0 +1 @@ +a diff --git a/puzzles/sequence/19/index.mdwn b/puzzles/sequence/19/index.mdwn new file mode 100644 index 0000000..ee8cdc1 --- /dev/null +++ b/puzzles/sequence/19/index.mdwn @@ -0,0 +1 @@ + 1 2 3 5 7 11 13 _ diff --git a/puzzles/sequence/19/key b/puzzles/sequence/19/key new file mode 100644 index 0000000..98d9bcb --- /dev/null +++ b/puzzles/sequence/19/key @@ -0,0 +1 @@ +17 diff --git a/puzzles/sequence/2/index.mdwn b/puzzles/sequence/2/index.mdwn new file mode 100644 index 0000000..88b06fc --- /dev/null +++ b/puzzles/sequence/2/index.mdwn @@ -0,0 +1 @@ + 1 10 11 100 101 110 _ _ diff --git a/puzzles/sequence/2/key b/puzzles/sequence/2/key new file mode 100644 index 0000000..9c657f0 --- /dev/null +++ b/puzzles/sequence/2/key @@ -0,0 +1 @@ +111 1000 diff --git a/puzzles/sequence/200/index.mdwn b/puzzles/sequence/200/index.mdwn new file mode 100644 index 0000000..b320eaa --- /dev/null +++ b/puzzles/sequence/200/index.mdwn @@ -0,0 +1 @@ + E D C D E E E . D D D . _ _ _ diff --git a/puzzles/sequence/200/key b/puzzles/sequence/200/key new file mode 100644 index 0000000..a1f32aa --- /dev/null +++ b/puzzles/sequence/200/key @@ -0,0 +1 @@ +E G G diff --git a/puzzles/sequence/25/index.mdwn b/puzzles/sequence/25/index.mdwn new file mode 100644 index 0000000..4224f45 --- /dev/null +++ b/puzzles/sequence/25/index.mdwn @@ -0,0 +1 @@ + 1 4 9 16 25 _ diff --git a/puzzles/sequence/25/key b/puzzles/sequence/25/key new file mode 100644 index 0000000..7facc89 --- /dev/null +++ b/puzzles/sequence/25/key @@ -0,0 +1 @@ +36 diff --git a/puzzles/sequence/300/index.mdwn b/puzzles/sequence/300/index.mdwn new file mode 100644 index 0000000..54f637c --- /dev/null +++ b/puzzles/sequence/300/index.mdwn @@ -0,0 +1 @@ + ┐ β”• β”š β”Ÿ _ diff --git a/puzzles/sequence/300/key b/puzzles/sequence/300/key new file mode 100644 index 0000000..4e818df --- /dev/null +++ b/puzzles/sequence/300/key @@ -0,0 +1 @@ +─ diff --git a/puzzles/sequence/35/index.mdwn b/puzzles/sequence/35/index.mdwn new file mode 100644 index 0000000..597a960 --- /dev/null +++ b/puzzles/sequence/35/index.mdwn @@ -0,0 +1 @@ + 1 1 2 3 5 8 _ _ diff --git a/puzzles/sequence/35/key b/puzzles/sequence/35/key new file mode 100644 index 0000000..126eeda --- /dev/null +++ b/puzzles/sequence/35/key @@ -0,0 +1 @@ +13 21 diff --git a/puzzles/sequence/400/index.mdwn b/puzzles/sequence/400/index.mdwn new file mode 100644 index 0000000..17eea60 --- /dev/null +++ b/puzzles/sequence/400/index.mdwn @@ -0,0 +1 @@ + 0000 0001 0011 0010 0110 0111 _ diff --git a/puzzles/sequence/400/key b/puzzles/sequence/400/key new file mode 100644 index 0000000..7d0d318 --- /dev/null +++ b/puzzles/sequence/400/key @@ -0,0 +1 @@ +0101 diff --git a/puzzles/sequence/450/index.mdwn b/puzzles/sequence/450/index.mdwn new file mode 100644 index 0000000..c992165 --- /dev/null +++ b/puzzles/sequence/450/index.mdwn @@ -0,0 +1 @@ + 04 aa 12 7f 99 03 ed c1 22 __ dc be e1 45 94 diff --git a/puzzles/sequence/450/key b/puzzles/sequence/450/key new file mode 100644 index 0000000..eeee65e --- /dev/null +++ b/puzzles/sequence/450/key @@ -0,0 +1 @@ +05 diff --git a/puzzles/sequence/50/index.mdwn b/puzzles/sequence/50/index.mdwn new file mode 100644 index 0000000..0a0db63 --- /dev/null +++ b/puzzles/sequence/50/index.mdwn @@ -0,0 +1 @@ + 4 8 15 16 23 _ diff --git a/puzzles/sequence/50/key b/puzzles/sequence/50/key new file mode 100644 index 0000000..d81cc07 --- /dev/null +++ b/puzzles/sequence/50/key @@ -0,0 +1 @@ +42 diff --git a/puzzles/sequence/600/index.mdwn b/puzzles/sequence/600/index.mdwn new file mode 100644 index 0000000..687ee22 --- /dev/null +++ b/puzzles/sequence/600/index.mdwn @@ -0,0 +1,26 @@ + 00000000 61 61 9e 23 01 0c 0c 40 0d 05 40 29 13 08 0d 01 + 00000010 05 0c 4e 40 40 33 0f 0d 05 40 19 05 01 12 13 40 + 00000020 01 07 0f 4d 4d 0e 05 16 05 12 40 0d 09 0e 04 40 + 00000030 08 0f 17 40 0c 0f 0e 07 6a 10 12 05 03 09 13 05 + 00000040 0c 19 4d 4d 08 01 16 09 0e 07 40 0c 09 14 14 0c + 00000050 05 40 0f 12 40 0e 0f 40 0d 0f 0e 05 19 40 09 0e + 00000060 40 0d 19 40 10 15 12 13 05 4c 40 01 0e 04 40 0e + 00000070 0f 14 08 09 0e 07 6a 10 01 12 14 09 03 15 0c 01 + 00000080 12 40 14 aa 61 62 9d 0f 40 09 0e 14 05 12 05 13 + 00000090 14 40 0d 05 40 0f 0e 40 13 08 0f 12 05 4c 40 29 + 000000a0 40 14 08 0f 15 07 08 14 40 29 40 17 0f 15 0c 04 + 000000b0 40 13 01 09 0c 40 01 02 0f 15 14 40 01 6a 0c 09 + 000000c0 14 14 0c 05 40 01 0e 04 40 13 05 05 40 14 08 05 + 000000d0 40 17 01 14 05 12 19 40 10 01 12 14 40 0f 06 40 + 000000e0 14 08 05 40 17 0f 12 0c 04 4e 40 40 29 14 40 09 + 000000f0 13 40 01 40 17 01 19 40 29 40 08 01 16 05 40 0f + 00000100 06 6a 04 12 09 16 09 3b 61 63 9c 0e 07 40 0f 06 + 00000110 06 40 14 08 05 40 13 10 0c 05 05 0e 40 01 0e 04 + 00000120 40 12 05 07 15 0c 01 14 09 0e 07 40 14 08 05 40 + 00000130 03 09 12 03 15 0c 01 14 09 0f 0e 4e 40 40 37 08 + 00000140 05 0e 05 16 05 12 40 29 6a 06 09 0e 04 40 0d 19 + 00000150 13 05 0c 06 40 07 12 0f 17 09 0e 07 40 07 12 09 + 00000160 0d 40 01 02 0f 15 14 40 14 08 05 40 0d 0f 15 14 + 00000170 08 5b 40 17 08 05 0e 05 16 05 12 40 09 14 40 09 + 00000180 13 40 01 40 04 01 0d 10 4c 6a 04 bb __ __ __ + diff --git a/puzzles/sequence/600/key b/puzzles/sequence/600/key new file mode 100644 index 0000000..7a6f1f6 --- /dev/null +++ b/puzzles/sequence/600/key @@ -0,0 +1 @@ +61 64 9b diff --git a/puzzles/sequence/700/index.mdwn b/puzzles/sequence/700/index.mdwn new file mode 100644 index 0000000..ea9dc0e --- /dev/null +++ b/puzzles/sequence/700/index.mdwn @@ -0,0 +1,5 @@ + C: 00 f1 00 b4 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 + S: 00 bf 00 f1 02 68 69 + C: 00 f3 00 bf 0b 68 6f 77 20 61 72 65 20 79 6f 75 + S: 00 ca 00 f3 0d 6e 6f 74 20 62 61 64 2c 20 79 6f 75 3f + C: __ __ __ __ __ 62 65 65 6e 20 77 6f 72 73 65 diff --git a/puzzles/sequence/700/key b/puzzles/sequence/700/key new file mode 100644 index 0000000..b26eb5a --- /dev/null +++ b/puzzles/sequence/700/key @@ -0,0 +1 @@ +01 00 00 ca 0a \ No newline at end of file diff --git a/puzzles/sequence/8/index.mdwn b/puzzles/sequence/8/index.mdwn new file mode 100644 index 0000000..40b13dd --- /dev/null +++ b/puzzles/sequence/8/index.mdwn @@ -0,0 +1 @@ + 66 67 70 71 72 73 74 75 76 77 _ diff --git a/puzzles/sequence/8/key b/puzzles/sequence/8/key new file mode 100644 index 0000000..29d6383 --- /dev/null +++ b/puzzles/sequence/8/key @@ -0,0 +1 @@ +100 diff --git a/puzzles/sequence/summary.txt b/puzzles/sequence/summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/puzzles/skynet/100/index.mdwn b/puzzles/skynet/100/index.mdwn new file mode 100644 index 0000000..2731c51 --- /dev/null +++ b/puzzles/skynet/100/index.mdwn @@ -0,0 +1,14 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +A user complains that their computer is slow and that all of their +email contacts are receving junk email from them. You find a file on +their system. Reverse engineer this file and answer the following +question: + +What is the name of the file that is copied to the system directory? +(Answer is lowercase, and only the name of the file) \ No newline at end of file diff --git a/puzzles/skynet/100/key b/puzzles/skynet/100/key new file mode 100644 index 0000000..9710187 --- /dev/null +++ b/puzzles/skynet/100/key @@ -0,0 +1 @@ +bbeagle.exe diff --git a/puzzles/skynet/100/malware.zip b/puzzles/skynet/100/malware.zip new file mode 100644 index 0000000..e84b564 Binary files /dev/null and b/puzzles/skynet/100/malware.zip differ diff --git a/puzzles/skynet/102/index.mdwn b/puzzles/skynet/102/index.mdwn new file mode 100644 index 0000000..60c331f --- /dev/null +++ b/puzzles/skynet/102/index.mdwn @@ -0,0 +1,13 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +A user complains that their computer is slow and that all of their +email contacts are receving junk email from them. You find a file on +their system. Reverse engineer this file and answer the following +question: + +What is the agent string sent to the call-home website? diff --git a/puzzles/skynet/102/key b/puzzles/skynet/102/key new file mode 100644 index 0000000..1584a27 --- /dev/null +++ b/puzzles/skynet/102/key @@ -0,0 +1 @@ +beagle_beagle diff --git a/puzzles/skynet/102/malware.zip b/puzzles/skynet/102/malware.zip new file mode 100644 index 0000000..e84b564 Binary files /dev/null and b/puzzles/skynet/102/malware.zip differ diff --git a/puzzles/skynet/200/index.mdwn b/puzzles/skynet/200/index.mdwn new file mode 100644 index 0000000..3e040a8 --- /dev/null +++ b/puzzles/skynet/200/index.mdwn @@ -0,0 +1,14 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +Your network is not working. The cheap routers that the network +engineers decided to buy (largely because of all the free booze +and golf trips the vendor paid for) are melting. Reverse engineer this +file and answer the following question: + +What is the original entry point (OEP)? +(Answer in all lower case, enter the virtual address in hex) \ No newline at end of file diff --git a/puzzles/skynet/200/key b/puzzles/skynet/200/key new file mode 100644 index 0000000..e46ab34 --- /dev/null +++ b/puzzles/skynet/200/key @@ -0,0 +1 @@ +402fcc diff --git a/puzzles/skynet/200/malware.zip b/puzzles/skynet/200/malware.zip new file mode 100644 index 0000000..0a969b0 Binary files /dev/null and b/puzzles/skynet/200/malware.zip differ diff --git a/puzzles/skynet/202/index.mdwn b/puzzles/skynet/202/index.mdwn new file mode 100644 index 0000000..fe92ea3 --- /dev/null +++ b/puzzles/skynet/202/index.mdwn @@ -0,0 +1,14 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +Your network is not working. The cheap routers that the network +engineers decided to buy (largely because of all the free booze +and golf trips the vendor paid for) are melting. Reverse engineer this +file and answer the following question: + +How does the program deliver its payload after the initial exploitation? +(Answer in all lower case) \ No newline at end of file diff --git a/puzzles/skynet/202/key b/puzzles/skynet/202/key new file mode 100644 index 0000000..63cfc22 --- /dev/null +++ b/puzzles/skynet/202/key @@ -0,0 +1 @@ +tftp diff --git a/puzzles/skynet/202/malware.zip b/puzzles/skynet/202/malware.zip new file mode 100644 index 0000000..0a969b0 Binary files /dev/null and b/puzzles/skynet/202/malware.zip differ diff --git a/puzzles/skynet/203/index.mdwn b/puzzles/skynet/203/index.mdwn new file mode 100644 index 0000000..cae30d0 --- /dev/null +++ b/puzzles/skynet/203/index.mdwn @@ -0,0 +1,13 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +Your network is not working. The cheap routers that the network +engineers decided to buy (largely because of all the free booze +and golf trips the vendor paid for) are melting. Reverse engineer this +file and answer the following question: + +What is the name of the first Windows API executed after unpacking? diff --git a/puzzles/skynet/203/key b/puzzles/skynet/203/key new file mode 100644 index 0000000..322b67a --- /dev/null +++ b/puzzles/skynet/203/key @@ -0,0 +1 @@ +FreeConsole diff --git a/puzzles/skynet/203/malware.zip b/puzzles/skynet/203/malware.zip new file mode 100644 index 0000000..0a969b0 Binary files /dev/null and b/puzzles/skynet/203/malware.zip differ diff --git a/puzzles/skynet/300/index.mdwn b/puzzles/skynet/300/index.mdwn new file mode 100644 index 0000000..2b76225 --- /dev/null +++ b/puzzles/skynet/300/index.mdwn @@ -0,0 +1,14 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +Your network is not working. The cheap routers that the network +engineers decided to buy (largely because of all the free booze +and golf trips the vendor paid for) are melting. Reverse engineer this +file and answer the following question: + +What is the name of the packer used to obfuscate this program? +(Answer in lower case) \ No newline at end of file diff --git a/puzzles/skynet/300/key b/puzzles/skynet/300/key new file mode 100644 index 0000000..40b7a6a --- /dev/null +++ b/puzzles/skynet/300/key @@ -0,0 +1 @@ +pecompact diff --git a/puzzles/skynet/300/malware.zip b/puzzles/skynet/300/malware.zip new file mode 100644 index 0000000..e83064e Binary files /dev/null and b/puzzles/skynet/300/malware.zip differ diff --git a/puzzles/skynet/301/index.mdwn b/puzzles/skynet/301/index.mdwn new file mode 100644 index 0000000..f887ae6 --- /dev/null +++ b/puzzles/skynet/301/index.mdwn @@ -0,0 +1,14 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +Your network is not working. The cheap routers that the network +engineers decided to buy (largely because of all the free booze +and golf trips the vendor paid for) are melting. Reverse engineer this +file and answer the following question: + +What is the address of the original entry point? +(Answer in all lower case, enter the virtual address in hex) \ No newline at end of file diff --git a/puzzles/skynet/301/key b/puzzles/skynet/301/key new file mode 100644 index 0000000..c22deab --- /dev/null +++ b/puzzles/skynet/301/key @@ -0,0 +1 @@ +4028de diff --git a/puzzles/skynet/301/malware.zip b/puzzles/skynet/301/malware.zip new file mode 100644 index 0000000..e83064e Binary files /dev/null and b/puzzles/skynet/301/malware.zip differ diff --git a/puzzles/skynet/302/index.mdwn b/puzzles/skynet/302/index.mdwn new file mode 100644 index 0000000..ef48518 --- /dev/null +++ b/puzzles/skynet/302/index.mdwn @@ -0,0 +1,13 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +Your network is not working. The cheap routers that the network +engineers decided to buy (largely because of all the free booze +and golf trips the vendor paid for) are melting. Reverse engineer this +file and answer the following question: + +What is the name of the second executed mutex? diff --git a/puzzles/skynet/302/key b/puzzles/skynet/302/key new file mode 100644 index 0000000..18d2446 --- /dev/null +++ b/puzzles/skynet/302/key @@ -0,0 +1 @@ +SkynetSasserVersionWithPingFast diff --git a/puzzles/skynet/302/malware.zip b/puzzles/skynet/302/malware.zip new file mode 100644 index 0000000..e83064e Binary files /dev/null and b/puzzles/skynet/302/malware.zip differ diff --git a/puzzles/skynet/400/index.mdwn b/puzzles/skynet/400/index.mdwn new file mode 100644 index 0000000..6ff0aea --- /dev/null +++ b/puzzles/skynet/400/index.mdwn @@ -0,0 +1,12 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +A user is complaining of random blue screens. Their computer is +running slowly. When you inspect the system you find a file named +malware.sys. Reverse engineer it and answer the following question. + +What user mode process does the kernel module inject into? \ No newline at end of file diff --git a/puzzles/skynet/400/key b/puzzles/skynet/400/key new file mode 100644 index 0000000..387eac7 --- /dev/null +++ b/puzzles/skynet/400/key @@ -0,0 +1 @@ +services.exe diff --git a/puzzles/skynet/400/malware.zip b/puzzles/skynet/400/malware.zip new file mode 100644 index 0000000..64d19cd Binary files /dev/null and b/puzzles/skynet/400/malware.zip differ diff --git a/puzzles/skynet/401/index.mdwn b/puzzles/skynet/401/index.mdwn new file mode 100644 index 0000000..b9829d1 --- /dev/null +++ b/puzzles/skynet/401/index.mdwn @@ -0,0 +1,12 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +A user is complaining of random blue screens. Their computer is +running slowly. When you inspect the system you find a file named +malware.sys. Reverse engineer it and answer the following question. + +What is the name of the DeviceObject for the kernel code? \ No newline at end of file diff --git a/puzzles/skynet/401/key b/puzzles/skynet/401/key new file mode 100644 index 0000000..622d880 --- /dev/null +++ b/puzzles/skynet/401/key @@ -0,0 +1 @@ +67678dj*&78 diff --git a/puzzles/skynet/401/malware.zip b/puzzles/skynet/401/malware.zip new file mode 100644 index 0000000..64d19cd Binary files /dev/null and b/puzzles/skynet/401/malware.zip differ diff --git a/puzzles/skynet/500/index.mdwn b/puzzles/skynet/500/index.mdwn new file mode 100644 index 0000000..28d6a42 --- /dev/null +++ b/puzzles/skynet/500/index.mdwn @@ -0,0 +1,13 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +A user complains that they are getting complaints of mass amounts of +penny stock scam emails coming from their computer. You happen to +extract this file from your network captures. + +What is the name of the file that is written to disk by this sample? +Enter your answer with all lower-case letters. \ No newline at end of file diff --git a/puzzles/skynet/500/key b/puzzles/skynet/500/key new file mode 100644 index 0000000..6197846 --- /dev/null +++ b/puzzles/skynet/500/key @@ -0,0 +1,2 @@ +c:\windows\system32:lzx32.sys + diff --git a/puzzles/skynet/500/laststage-unpacker.exe b/puzzles/skynet/500/laststage-unpacker.exe new file mode 100644 index 0000000..db42b4f Binary files /dev/null and b/puzzles/skynet/500/laststage-unpacker.exe differ diff --git a/puzzles/skynet/500/malware.zip b/puzzles/skynet/500/malware.zip new file mode 100644 index 0000000..759c395 Binary files /dev/null and b/puzzles/skynet/500/malware.zip differ diff --git a/puzzles/skynet/501/index.mdwn b/puzzles/skynet/501/index.mdwn new file mode 100644 index 0000000..7285ef0 --- /dev/null +++ b/puzzles/skynet/501/index.mdwn @@ -0,0 +1,15 @@ +This category contains live samples of real malware. If you participate +in this category, you agree to take all proper precautions: running in +a virtual machine, properly cleaning your system afterwards, and not +exposing any networks to these samples. + +YOU HAVE BEEN WARNED! + +A user complains that they are getting complaints of mass amounts of +penny stock scam emails coming from their computer. You happen to +extract this file from your network captures. + +You'll need the previously dropped file for this exercise. + +What is the tag identifier for the ExAllocatePoolWithTag? +(enter the ascii text) \ No newline at end of file diff --git a/puzzles/skynet/501/key b/puzzles/skynet/501/key new file mode 100644 index 0000000..ea5431b --- /dev/null +++ b/puzzles/skynet/501/key @@ -0,0 +1 @@ + kdD diff --git a/puzzles/skynet/501/laststage-unpacker.exe b/puzzles/skynet/501/laststage-unpacker.exe new file mode 100644 index 0000000..db42b4f Binary files /dev/null and b/puzzles/skynet/501/laststage-unpacker.exe differ diff --git a/puzzles/skynet/501/malware.zip b/puzzles/skynet/501/malware.zip new file mode 100644 index 0000000..759c395 Binary files /dev/null and b/puzzles/skynet/501/malware.zip differ diff --git a/puzzles/skynet/summary.txt b/puzzles/skynet/summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/puzzles/survey/1000000/,submit.cgi b/puzzles/survey/1000000/,submit.cgi new file mode 100755 index 0000000..39a9a08 --- /dev/null +++ b/puzzles/survey/1000000/,submit.cgi @@ -0,0 +1,25 @@ +#! /usr/bin/env python3 + +import cgi +import time +import os + +f = cgi.FieldStorage() +if f.getfirst('submit'): + print('Content-type: text/plain') + print() + print('Thanks for filling in the survey.') + print() + try: + fn = '/var/lib/ctf/survey/%s.%d.%d.txt' % (time.strftime('%Y-%m-%d'), time.time(), os.getpid()) + o = open(fn, 'w') + for k in f.keys(): + o.write('%s: %r\n' % (k, f.getlist(k))) + except IOError: + pass + print('The key is:') + print(' quux blorb frotz') +else: + print('Content-type: text/plain') + print() + print('You need to actually fill in the form to get the key.') diff --git a/puzzles/survey/1000000/,survey.html b/puzzles/survey/1000000/,survey.html new file mode 100644 index 0000000..0ed037b --- /dev/null +++ b/puzzles/survey/1000000/,survey.html @@ -0,0 +1,44 @@ + + + + + Survey + + + +
+
    +
  • + Did you have any trouble figuring out how to play? + +
  • + +
  • + How difficult were the puzzles? + +
  • +
+ +

+ Please use the provided space for any additional comments. +

+ +

+ Thanks for your feedback! We hope you had fun and learned + something! +

+ +
+ + + diff --git a/puzzles/survey/1000000/index.mdwn b/puzzles/survey/1000000/index.mdwn new file mode 100644 index 0000000..486aafe --- /dev/null +++ b/puzzles/survey/1000000/index.mdwn @@ -0,0 +1,8 @@ +Thanks for playing Capture The Flag! We would like to know what you +think of the game; please fill out this survey and you will +recieve a key redeemable for **ONE MILLION POINTS**. + + + Survey + diff --git a/puzzles/survey/1000000/key b/puzzles/survey/1000000/key new file mode 100644 index 0000000..0bb4c84 --- /dev/null +++ b/puzzles/survey/1000000/key @@ -0,0 +1 @@ +quux blorb frotz diff --git a/puzzles/survey/summary.txt b/puzzles/survey/summary.txt new file mode 100644 index 0000000..e69de29 diff --git a/puzzles/webapp/10/,binary.png b/puzzles/webapp/10/,binary.png new file mode 100644 index 0000000..81cecbf Binary files /dev/null and b/puzzles/webapp/10/,binary.png differ diff --git a/puzzles/webapp/10/,ctf.css b/puzzles/webapp/10/,ctf.css new file mode 100644 index 0000000..4f1b798 --- /dev/null +++ b/puzzles/webapp/10/,ctf.css @@ -0,0 +1,62 @@ +html,body { + height: 100%; + min-height: 100%; + background-color: #000000; + background-image: url(",binary.png"); + background-repeat: repeat; + margin: 0; + padding: 0; +} + +#wrapper { + min-height: 100%; + height: 100%; + width: 800px; + margin: 0 auto; + border-left: 2px solid #009900; + border-right: 2px solid #009900; + font: .9em monospace; + color: #009900; + padding: 0; + background: #000; +} + +#content { + padding: 2em 1.5em 2em 1.5em; +} + +#footer { + padding: 0; + margin: 0; + height: 2em; + line-height: 2em; + width: 800px; + text-align: center; +} + +input { + background-color: #222; + color: #fff; + border: 1px solid #009900; + padding: 1px 2px 1px 2px; +} + +h1,h2,h3,h4 { + padding-bottom: 5px; +} + +.vertsep { + width: 100%; + height: 1px; + padding: 0; + margin: 2em auto 2em auto; + border-bottom: 1px dotted #222; +} + +.error { + padding: 1em; + background: #fff; + color: red; + border: 1px solid red; + font-weight: bold; +} diff --git a/puzzles/webapp/10/1.cgi b/puzzles/webapp/10/1.cgi new file mode 100755 index 0000000..91ddffd --- /dev/null +++ b/puzzles/webapp/10/1.cgi @@ -0,0 +1,32 @@ +#!/usr/bin/python + +import cgi +import cgitb + +print 'Content-Type: text/html' +print '' + +print ''' + + + 1 + + + + +
+
+

Web Application Challenge 1

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+
+ +
+ + +''' + diff --git a/puzzles/webapp/10/key b/puzzles/webapp/10/key new file mode 100644 index 0000000..c6f8085 --- /dev/null +++ b/puzzles/webapp/10/key @@ -0,0 +1 @@ +ktFfb8R1Bw diff --git a/puzzles/webapp/20/,binary.png b/puzzles/webapp/20/,binary.png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/20/,binary.png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/20/,ctf.css b/puzzles/webapp/20/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/20/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/20/2.cgi b/puzzles/webapp/20/2.cgi new file mode 100755 index 0000000..3e9b6cd --- /dev/null +++ b/puzzles/webapp/20/2.cgi @@ -0,0 +1,48 @@ +#!/usr/bin/python + +import cgi +import cgitb +cgitb.enable(context=10) + +fields = cgi.FieldStorage() + +print 'Content-Type: text/html' +print '' + + +print ''' + + + 2 + + + +
+
+

Web Application Challenge 2

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+

Question: How many geeks does it take to break a CGI?

+''' + +# key = uq4G4dXrpx +if (fields.has_key('num')): + print ''' +

You entered %d.

+ ''' % int(fields['num'].value) + +print ''' +
+ Enter an integer: +
+
+ +
+ + +''' + diff --git a/puzzles/webapp/20/key b/puzzles/webapp/20/key new file mode 100644 index 0000000..f460886 --- /dev/null +++ b/puzzles/webapp/20/key @@ -0,0 +1 @@ +uq4G4dXrpx diff --git a/puzzles/webapp/30/,binary.png b/puzzles/webapp/30/,binary.png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/30/,binary.png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/30/,ctf.css b/puzzles/webapp/30/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/30/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/30/3.cgi b/puzzles/webapp/30/3.cgi new file mode 100755 index 0000000..63f495b --- /dev/null +++ b/puzzles/webapp/30/3.cgi @@ -0,0 +1,73 @@ +#!/usr/bin/python + +import cgi +import cgitb +cgitb.enable(context=10) + +fields = cgi.FieldStorage() + +print 'Content-Type: text/html' +print '' + + +print ''' + + + 3 + + + +
+
+

Web Application Challenge 3

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+ +
+''' + +PRODUCT_NAME = "Monkey of some kind" + +def purchase_success(quantity): + print ''' +

Congratulations, your order for %d "%s" has been placed.

+ ''' % (quantity, PRODUCT_NAME) + +# key = BRrHdtdADI +if fields.has_key('quantity') and fields.has_key('product') and fields['product'].value == PRODUCT_NAME: + product = fields['product'].value + quantity = int(fields['quantity'].value) + + purchase_success(quantity) +else: + print ''' + +

SALE: %s

+

Use the order form below to place an order.

+ +
+ How many would you like? + +

+ + +
+ ''' % (PRODUCT_NAME, PRODUCT_NAME) + +print ''' + +
+ +
+ + +''' + diff --git a/puzzles/webapp/30/key b/puzzles/webapp/30/key new file mode 100644 index 0000000..4023e13 --- /dev/null +++ b/puzzles/webapp/30/key @@ -0,0 +1 @@ +BRrHdtdADI diff --git a/puzzles/webapp/40/,binary.png b/puzzles/webapp/40/,binary.png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/40/,binary.png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/40/,ctf.css b/puzzles/webapp/40/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/40/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/40/4.cgi b/puzzles/webapp/40/4.cgi new file mode 100755 index 0000000..55d66fe --- /dev/null +++ b/puzzles/webapp/40/4.cgi @@ -0,0 +1,76 @@ +#!/usr/bin/python + +import os +import cgi +import cgitb +cgitb.enable(context=10) + +if os.environ.has_key('QUERY_STRING'): + os.environ['QUERY_STRING'] = '' + +fields = cgi.FieldStorage() + +print 'Content-Type: text/html' +print '' + +print ''' + + + 4 + + + +
+
+

Web Application Challenge 4

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+ +
+''' + +PRODUCT_NAME = "Unidentifiable garbage" + +def purchase_success(quantity): + print ''' +

Congratulations, your order for %d "%s" has been placed.

+ ''' % (quantity, PRODUCT_NAME) + +# key = 765JBo4B54 +if fields.has_key('quantity') and fields.has_key('product') and fields['product'].value == PRODUCT_NAME: + product = fields['product'].value + quantity = int(fields['quantity'].value) + + purchase_success(quantity) +else: + print ''' + +

SALE: %s

+

Use the order form below to place an order.

+ +
+ How many would you like? + +

+ + +
+ ''' % (PRODUCT_NAME, PRODUCT_NAME) + +print ''' + +
+ +
+ + +''' + diff --git a/puzzles/webapp/40/key b/puzzles/webapp/40/key new file mode 100644 index 0000000..606cf1d --- /dev/null +++ b/puzzles/webapp/40/key @@ -0,0 +1 @@ +765JBo4B54 diff --git a/puzzles/webapp/50/,binary,png b/puzzles/webapp/50/,binary,png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/50/,binary,png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/50/,ctf.css b/puzzles/webapp/50/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/50/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/50/5.cgi b/puzzles/webapp/50/5.cgi new file mode 100755 index 0000000..23cc4e7 --- /dev/null +++ b/puzzles/webapp/50/5.cgi @@ -0,0 +1,89 @@ +#!/usr/bin/python + +import os +import cgi +import cgitb +cgitb.enable(context=10) + +if os.environ.has_key('QUERY_STRING'): + os.environ['QUERY_STRING'] = '' + +fields = cgi.FieldStorage() + +print 'Content-Type: text/html' +print '' + +print ''' + + + 5 + + + +
+
+

Web Application Challenge 5

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+ +
+''' + +PRODUCT_NAME = "Alex Brugh" +QUANT_LIMIT = 1 + +def purchase_success(quantity): + print ''' +

Congratulations, your order for %d "%s" has been placed.

+ ''' % (quantity, PRODUCT_NAME) + +class InvalidQuantityError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +quantity = None +if fields.has_key('quantity') and fields.has_key('product') and fields['product'].value == PRODUCT_NAME: + product = fields['product'].value + try: + quantity = int(fields['quantity'].value) + if quantity > QUANT_LIMIT: + # key = eVkIwHzOok + raise InvalidQuantityError("%d is not a valid quantity (limit %d)" % (quantity, QUANT_LIMIT)) + except ValueError: + print ''' +

There was an error with your order request. Sorry.

+ ''' + quantity = None + +if quantity is not None: + purchase_success(quantity) +else: + print ''' + +

SALE: %s

+

Use the order form below to place an order.

+ +
+ Orders for "%s" are limited to 1 per customer. +

+ + + +
+ ''' % (PRODUCT_NAME, PRODUCT_NAME, PRODUCT_NAME) + +print ''' + +
+ +
+ + +''' + diff --git a/puzzles/webapp/50/key b/puzzles/webapp/50/key new file mode 100644 index 0000000..042dd52 --- /dev/null +++ b/puzzles/webapp/50/key @@ -0,0 +1 @@ +eVkIwHzOok diff --git a/puzzles/webapp/60/,binary.png b/puzzles/webapp/60/,binary.png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/60/,binary.png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/60/,ctf.css b/puzzles/webapp/60/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/60/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/60/6.cgi b/puzzles/webapp/60/6.cgi new file mode 100755 index 0000000..75e192b --- /dev/null +++ b/puzzles/webapp/60/6.cgi @@ -0,0 +1,72 @@ +#!/usr/bin/python + +import os +import cgi +import cgitb +cgitb.enable(context=10) + +#if os.environ.has_key('QUERY_STRING'): +# os.environ['QUERY_STRING'] = '' + +fields = cgi.FieldStorage() + +import Cookie +c = Cookie.SimpleCookie() +c['key'] = 'QJebByJaKX' +c['content'] = '

Maybe I should have used sessions...

' + +print 'Content-Type: text/html\n%s\n\n\n' % c +print '' + +print ''' + + + 6 + + + + +
+
+

Web Application Challenge 6

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+ +
+
+''' + +print ''' +
+ +
+ + +''' + diff --git a/puzzles/webapp/60/key b/puzzles/webapp/60/key new file mode 100644 index 0000000..f235990 --- /dev/null +++ b/puzzles/webapp/60/key @@ -0,0 +1 @@ +QJebByJaKX diff --git a/puzzles/webapp/70/,binary.png b/puzzles/webapp/70/,binary.png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/70/,binary.png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/70/,ctf.css b/puzzles/webapp/70/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/70/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/70/7.cgi b/puzzles/webapp/70/7.cgi new file mode 100755 index 0000000..1bfdf64 --- /dev/null +++ b/puzzles/webapp/70/7.cgi @@ -0,0 +1,86 @@ +#!/usr/bin/python + +import os +import cgi +import cgitb +cgitb.enable(context=10) + +#if os.environ.has_key('QUERY_STRING'): +# os.environ['QUERY_STRING'] = '' + +fields = cgi.FieldStorage() + +import Cookie +c = Cookie.SimpleCookie(os.environ.get('HTTP_COOKIE', '')) + +content = { + 'joke1' : '

An infinite number of mathematicians walk into a bar. The first one orders a beer. The second orders half a beer. The third, a quarter of a beer. The bartender says You are all idiots! and pours two beers.

', + 'joke2' : '

Two atoms are talking. One of them says I think I lost an electron! and the other says Are you sure? The first replies Yeah, I am positive!

', +} + +if c.has_key('content_name') and c.has_key('content'): + k = c['content_name'].value + try: + c['content'] = content[k] + except KeyError: + c['content'] = '

key = s4nNlaMScV

' +else: + c['content_name'] = 'joke1'; + c['content'] = content['joke1'] + + +print 'Content-Type: text/html\n%s\n\n\n' % c +print '' + +print ''' + + + 7 + + + + +
+
+

Web Application Challenge 7

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+ +
+
+''' + +print ''' +
+ +
+ + +''' + diff --git a/puzzles/webapp/70/key b/puzzles/webapp/70/key new file mode 100644 index 0000000..16f48e0 --- /dev/null +++ b/puzzles/webapp/70/key @@ -0,0 +1 @@ +s4nNlaMScV diff --git a/puzzles/webapp/80/,binary.png b/puzzles/webapp/80/,binary.png new file mode 120000 index 0000000..36053bd --- /dev/null +++ b/puzzles/webapp/80/,binary.png @@ -0,0 +1 @@ +../10/,binary.png \ No newline at end of file diff --git a/puzzles/webapp/80/,ctf.css b/puzzles/webapp/80/,ctf.css new file mode 120000 index 0000000..19b2533 --- /dev/null +++ b/puzzles/webapp/80/,ctf.css @@ -0,0 +1 @@ +../10/,ctf.css \ No newline at end of file diff --git a/puzzles/webapp/80/,jokes/bar b/puzzles/webapp/80/,jokes/bar new file mode 100644 index 0000000..9e0dfe3 --- /dev/null +++ b/puzzles/webapp/80/,jokes/bar @@ -0,0 +1,4 @@ +

An unsigned integer walks into a bar and orders a drink.
+The bartender delivers it and says, "Is something wrong?"
+The int looks up and replies, "Parity error."
+"Ah," the bartender replies, "I thought you looked a bit off."

diff --git a/puzzles/webapp/80/,jokes/binary b/puzzles/webapp/80/,jokes/binary new file mode 100644 index 0000000..58ddc26 --- /dev/null +++ b/puzzles/webapp/80/,jokes/binary @@ -0,0 +1,2 @@ +

There are 10 types of people in the world: those who understand binary and those who don't.

+ diff --git a/puzzles/webapp/80/,jokes/christmas b/puzzles/webapp/80/,jokes/christmas new file mode 100644 index 0000000..3bade53 --- /dev/null +++ b/puzzles/webapp/80/,jokes/christmas @@ -0,0 +1,2 @@ +

Why do programmers confuse Halloween and Christmas?

+Because OCT 31 == DEC 25!

diff --git a/puzzles/webapp/80/,jokes/help b/puzzles/webapp/80/,jokes/help new file mode 100644 index 0000000..e79c54b --- /dev/null +++ b/puzzles/webapp/80/,jokes/help @@ -0,0 +1,2 @@ +

Once a programmer drowned in the sea. Many people were at the beach at the time, +but the programmer was shouting "F1! F1!" and nobody understood it.

diff --git a/puzzles/webapp/80/,jokes/java b/puzzles/webapp/80/,jokes/java new file mode 100644 index 0000000..9c5e84d --- /dev/null +++ b/puzzles/webapp/80/,jokes/java @@ -0,0 +1,6 @@ +

"Knock, Knock."
+"Who's there?"
+
+... long pause ...
+
+"Java."

diff --git a/puzzles/webapp/80/,makedb.py b/puzzles/webapp/80/,makedb.py new file mode 100755 index 0000000..d6c886f --- /dev/null +++ b/puzzles/webapp/80/,makedb.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python2.6 + +import os +import sys +import sqlite3 +import base64 +import stat + +# new db +if os.path.exists(',zomg.sqlite3'): + os.remove(',zomg.sqlite3') +db = sqlite3.connect(',zomg.sqlite3') +cur = db.cursor() + +# pics table +cur.execute('create table pics(id integer primary key, data blob)') +paths = os.listdir(',pics/') +for path in paths: + f = open(os.path.join(',pics/', path), 'rb') + data = f.read() + f.close() + encoded = base64.encodestring(data) + html = '' % encoded + cur.execute('insert into pics(data) values(?)', (html,)) + +# jokes table +cur.execute('create table jokes(id integer primary key, data text)') +paths = os.listdir(',jokes/') +for path in paths: + f = open(os.path.join(',jokes/', path), 'r') + html = f.read() + f.close() + cur.execute('insert into jokes(data) values(?)', (html,)) + +# key +cur.execute('create table key(id integer primary key, data text)') +for k in [None, None, None, None, None, 'dmW5f9P54e']: + cur.execute('insert into key(data) values(?)', (k,)) + +# clean up +db.commit() +cur.close() +db.close() + +os.chmod(',zomg.sqlite3', stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) diff --git a/puzzles/webapp/80/,pics/90s.jpg b/puzzles/webapp/80/,pics/90s.jpg new file mode 100644 index 0000000..5e31456 Binary files /dev/null and b/puzzles/webapp/80/,pics/90s.jpg differ diff --git a/puzzles/webapp/80/,pics/melon.jpg b/puzzles/webapp/80/,pics/melon.jpg new file mode 100644 index 0000000..4ed92f5 Binary files /dev/null and b/puzzles/webapp/80/,pics/melon.jpg differ diff --git a/puzzles/webapp/80/,pics/pumpkin.jpg b/puzzles/webapp/80/,pics/pumpkin.jpg new file mode 100644 index 0000000..cca7efc Binary files /dev/null and b/puzzles/webapp/80/,pics/pumpkin.jpg differ diff --git a/puzzles/webapp/80/,pics/ruth.jpg b/puzzles/webapp/80/,pics/ruth.jpg new file mode 100644 index 0000000..46ea699 Binary files /dev/null and b/puzzles/webapp/80/,pics/ruth.jpg differ diff --git a/puzzles/webapp/80/,pics/soccer.jpg b/puzzles/webapp/80/,pics/soccer.jpg new file mode 100644 index 0000000..b2f1ba7 Binary files /dev/null and b/puzzles/webapp/80/,pics/soccer.jpg differ diff --git a/puzzles/webapp/80/,zomg.sqlite3 b/puzzles/webapp/80/,zomg.sqlite3 new file mode 100644 index 0000000..4b5bf1f Binary files /dev/null and b/puzzles/webapp/80/,zomg.sqlite3 differ diff --git a/puzzles/webapp/80/8.cgi b/puzzles/webapp/80/8.cgi new file mode 100755 index 0000000..280b5d3 --- /dev/null +++ b/puzzles/webapp/80/8.cgi @@ -0,0 +1,150 @@ +#!/usr/bin/python + +import os +import cgi +import cgitb +import sqlite3 +cgitb.enable(context=10) + +if os.environ.has_key('QUERY_STRING'): + os.environ['QUERY_STRING'] = '' + +fields = cgi.FieldStorage() + +q = None +if fields.has_key('q'): + q = fields['q'].value + +if q is not None: + print 'Content-Type: text/html\n' + try: + db = sqlite3.connect(',zomg.sqlite3') + cur = db.cursor() + cur.execute(q) + results = cur.fetchall() + + print '' + for r in results: + print '' + for thing in r: + print '' % thing + print '' + print '
%s
' + + except Exception: + print '

Invalid query: %s

' % q + +else: + print 'Content-Type: text/html\n' + print '' + + print ''' + + + 8 + + + + +
+
+

Web Application Challenge 8

+

Through some manipulation or interpretation of this CGI script + and the HTML page(s) that it generates, a 10 character key can be + found.

+

Find the key!

+ +
+

Database Query Wizard

+

Use the form below to retrieve data from the database. Select the + type of data that you would like to view and the number of database + entries to retrieve and then click on the "Query" button.

+ +
+
+ Topic: +

+ # Results: +

+ +
+ +
+
+ +
+ + + ''' + diff --git a/puzzles/webapp/80/key b/puzzles/webapp/80/key new file mode 100644 index 0000000..f77d1bd --- /dev/null +++ b/puzzles/webapp/80/key @@ -0,0 +1 @@ +dmW5f9P54e diff --git a/puzzles/webapp/summary.txt b/puzzles/webapp/summary.txt new file mode 100644 index 0000000..03f7ce0 --- /dev/null +++ b/puzzles/webapp/summary.txt @@ -0,0 +1,17 @@ +10: the key is in the generated source. +20: enter a non-integer into form field and submit. the key is in the resulting + traceback. +30: change the value in the GET request to a non-integer. the key is in the + resulting traceback. +40: change the value in the POST request to a non-integer. the key is in the + resulting traceback. +50: change the quantity value (hidden form field) to something greater than the + stated quantity limit. the key is in the resulting traceback. entering non- + integers is caught and handled, so that no longer works. +60: the key is in the cookie. note the javascript that reads a value from the + cookie, hopefully causing the player to take a look at the cookie. +70: modify the cookie's content_name field to something invalid, reload the page + and the key will be printed on the page. +80: an sql query is being constructed in javascript from form fields. change the + form fields such that the query is SELECT * FROM key LIMIT 6 and the key will + be displayed. diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..87813d8 --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +#! /usr/bin/python + +from distutils.core import setup + +setup(name='ctf', + version='1.0', + description='Capture The Flag contest', + author='Neale Pickett', + author_email='neale@lanl.gov', + url='http://dirtbags.net/ctf/', + packages=['ctf', 'tanks']) diff --git a/site-packages/badmath.py b/site-packages/badmath.py new file mode 100644 index 0000000..e8df9b0 --- /dev/null +++ b/site-packages/badmath.py @@ -0,0 +1,125 @@ +import random +import math + +# Not defined in older Python's math libs +def multiply(a, b): + return a * b + +def factorial(n): + return reduce(multiply, range(1, n+1)) + +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: int(math.degrees(b + a)), + lambda a, b: ~(a & b), + lambda a, b: ~(a ^ b), + lambda a, b: a + b - a%b, + lambda a, b: (a > 0) and (factorial(a)//factorial(a-b)) or 0, + lambda a, b: (b%a) * (a%b), + lambda a, b: 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: a == b and 5 or 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 diff --git a/site-packages/canvas.py b/site-packages/canvas.py new file mode 100644 index 0000000..3374304 --- /dev/null +++ b/site-packages/canvas.py @@ -0,0 +1,40 @@ +#! /usr/bin/python + +import png +from array import array + +class Canvas: + def __init__(self, width, height, bg=(0,0,0)): + self.width = width + self.height = height + + # Build the canvas using arrays, which are way quick + row = array('B') + for i in xrange(self.width): + row.extend(bg) + + self.bytes = array('B') + for i in xrange(self.height): + self.bytes.extend(row) + + def get(self, x, y): + offs = ((y*self.width)+x)*3 + return self.bytes[offs:offs+3] + + def set(self, x, y, pixel): + offs = ((y*self.width)+x)*3 + for i in range(3): + self.bytes[offs+i] = pixel[i] + + def write(self, f): + p = png.Writer(self.width, self.height) + p.write_array(f, self.bytes) + +if __name__ == '__main__': + width = 800 + height = 600 + + c = Canvas(width, height) + for x in range(width): + c.set(x, x % height, (x%256,(x*2)%256,(x*3)%256)) + c.write(open('foo.png', 'wb')) diff --git a/site-packages/irc.py b/site-packages/irc.py new file mode 100644 index 0000000..432800c --- /dev/null +++ b/site-packages/irc.py @@ -0,0 +1,541 @@ +#! /usr/bin/python + +import asynchat +import asyncore +import socket +import sys +import traceback +import time + +channel_prefixes = '+#&' + +class IRCHandler(asynchat.async_chat): + """IRC Server connection. + + This is the one you want to derive your connection classes from. + + """ + + debug = False + heartbeat_interval = 1 # seconds per heartbeat + + def __init__(self, host=None, nick=None, gecos=None): + asynchat.async_chat.__init__(self) + self.line = '' + self.timers = [] + self.last_heartbeat = 0 + self.set_terminator('\r\n') + if host: + self.open_connection(host, nick, gecos) + + def dbg(self, msg): + if self.debug: + print(msg) + + def open_connection(self, host, nick, gecos): + self.nick = nick + self.gecos = gecos + self.host = host + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect(host) + + def handle_connect(self): + self.write(['NICK', self.nick]) + self.write(['USER', self.nick, '+iw', self.nick], self.gecos) + + def connect(self, host): + self.waiting = False + asynchat.async_chat.connect(self, host) + + def heartbeat(self): + """Invoke all timers.""" + + if not self.timers: + return + timers, self.timers = self.timers, [] + now = time.time() + for t, cb in timers: + if t > now: + self.timers.append((t, cb)) + else: + cb() + + def add_timer(self, secs, callback): + """After secs seconds, call callback""" + self.timers.append((time.time() + secs, callback)) + + def readable(self): + """Called by asynchat to see if we're readable. + + We hook our heartbeat in here. + """ + + now = time.time() + if now > self.last_heartbeat + self.heartbeat_interval: + self.heartbeat() + self.last_heartbeat = now + + if self.connected: + return asynchat.async_chat.readable(self) + else: + return False + + def collect_incoming_data(self, data): + """Called by asynchat when data arrives""" + self.line += data + + def found_terminator(self): + """Called by asynchat when it finds the terminating character. + """ + line = self.line.decode('utf-8') + self.line = '' + self.parse_line(line) + + def write(self, args, text=None): + """Send out an IRC command + + This function helps to prevent you from shooting yourself in the + foot, by forcing you to send commands that are in a valid format + (although it doesn't check the validity of the actual commands). + + As we all know, IRC commands take the form + + :COMMAND ARG1 ARG2 ARG3 ... :text string + + where 'text string' is optional. Well, that's exactly how this + function works. Args is a list of length at least one, and text + string is a string. + + write(['PRIVMSG', nick], 'Hello 12') + + will send + + PRIVMSG nick :Hello 12 + + """ + + cmdstr = ' '.join(args) + if text: + cmdstr = '%s :%s' % (cmdstr, text) + self.dbg('-> %s' % cmdstr) + try: + line = '%s\n' % cmdstr + self.send(line.encode('utf-8')) + except socket.error: + pass + + + def parse_line(self, line): + """Parse a server-provided line + + This does all the magic of parsing those ill-formatted IRC + messages. It will also decide if a PRIVMSG or NOTICE is using + CTCP (the client-to-client protocol, which by convention is any + of the above messages with ^A on both ends of the text. + + This function goes on to invoke self.eval_triggers on the parsed + data like this: + + self.eval_triggers(operation, arguments, text) + + where operation and text are strings, and arguments is a list. + + It returns the same tuple (op, args, text). + + """ + + if (line[0] == ':'): + with_uname = 1 + line = line [1:] + else: + with_uname = 0 + try: + [args, text] = line.split(' :', 1) + args = args.split() + except ValueError: + args = line.split() + text = '' + if (with_uname != 1): + op = args[0] + elif ((args[1] in ["PRIVMSG", "NOTICE"]) and + (text and (text[0] == '\001') and (text[-1] == '\001'))): + op = "C" + args[1] + text = text[1:-1] + else: + op = args[1] + self.dbg("<- %s %s %s" % (op, args, text)) + self.handle(op, args, text) + return (op, args, text) + + + def handle(self, op, args, text): + """Take action on a server message + + Right now, just invokes + + self.do_[op](args, text) + + where [op] is the operation passed in. + + This is a good method to overload if you want a really advanced + client supporting bindings. + + """ + try: + method = getattr(self, "do_" + lower(op)) + except AttributeError: + self.dbg("Unhandled: %s" % (op, args, text)) + return + method(args, text) + + +class Recipient: + """Abstract recipient object""" + + def __init__(self, interface, name): + self._interface = interface + self._name = name + + def __repr__(self): + return 'Recipient(%s)' % self.name() + + def name(self): + return self._name + + def is_channel(self): + return False + + def write(self, cmd, addl): + """Write a raw IRC command to our interface""" + + self._interface.write(cmd, addl) + + def cmd(self, cmd, text): + """Send a command to ourself""" + + self.write([cmd, self._name], text) + + def msg(self, text): + """Tell the recipient something""" + + self.cmd("PRIVMSG", text) + + def notice(self, text): + """Send a notice to the recipient""" + + self.cmd("NOTICE", text) + + def ctcp(self, command, text): + """Send a CTCP command to the recipient""" + + return self.msg("\001%s %s\001" % (command.upper(), text)) + + def act(self, text): + """Send an action to the recipient""" + + return self.ctcp("ACTION", text) + + def cnotice(self, command, text): + """Send a CTCP notice to the recipient""" + + return self.notice("\001%s %s\001" % (command.upper(), text)) + +class Channel(Recipient): + def __repr__(self): + return 'Channel(%s)' % self.name() + + def is_channel(self): + return True + +class User(Recipient): + def __init__(self, interface, name, user, host, op=False): + Recipient.__init__(self, interface, name) + self.user = user + self.host = host + self.op = op + + def __repr__(self): + return 'User(%s, %s, %s)' % (self.name(), self.user, self.host) + +def recipient(interface, name): + if name[0] in channel_prefixes: + return Channel(interface, name) + else: + return User(interface, name, None, None) + +class SmartIRCHandler(IRCHandler): + """This is like the IRCHandler, except it creates Recipient objects + for IRC messages. The intent is to make it easier to write stuff + without knowledge of the IRC protocol. + + """ + + def recipient(self, name): + return recipient(self, name) + + def err(self, exception): + if self.debug: + traceback.print_exception(*exception) + + def handle(self, op, args, text): + """Parse more, creating objects and stuff + + makes a call to self.handle_op(sender, forum, addl) + + sender is always a Recipient object; if you want to reply + privately, you can send your reply to sender. + + forum is a Recipient object corresponding with the forum over + which the message was carried. For user-to-user PRIVMSG and + NOTICE commands, this is the same as sender. For those same + commands sent to a channel, it is the channel. Thus, you can + always send a reply to forum, and it will be sent back in an + appropriate manner (ie. the way you expect). + + addl is a tuple, containing additional information which might + be relelvant. Here's what it will contain, based on the server + operation: + + op | addl + ---------+---------------- + PRIVMSG | text of the message + NOTICE | text of the notice + CPRIVMSG | CTCP command, text of the command + CNOTICE | CTCP response, text of the response + KICK * | victim of kick, kick text + MODE * | all mode args + JOIN * | empty + PART * | empty + QUIT | quit message + PING | ping text + NICK ! | old nickname + others | all arguments; text is last element + + * The forum in these items is the channel to which the action + pertains. + ! The sender for the NICK command is the *new* nickname. This + is so you can send messages to the sender object and they'll + go to the right place. + """ + + try: + sender = User(self, *unpack_nuhost(args)) + except ValueError: + sender = None + forum = None + addl = () + + if op in ("PRIVMSG", "NOTICE"): + # PRIVMSG ['neale!~user@127.0.0.1', 'PRIVMSG', '#hydra'] firebot, foo + # PRIVMSG ['neale!~user@127.0.0.1', 'PRIVMSG', 'firebot'] firebot, foo + try: + forum = self.recipient(args[2]) + if not forum.is_channel(): + forum = sender + addl = (text,) + except IndexError: + addl = (text, args[1]) + elif op in ("CPRIVMSG", "CNOTICE"): + forum = self.recipient(args[2]) + splits = text.split(" ") + if splits[0] == "DCC": + op = "DC" + op + addl = (splits[1],) + tuple(splits[2:]) + else: + addl = (splits[0],) + tuple(splits[1:]) + elif op in ("KICK",): + forum = self.recipient(args[2]) + addl = (self.recipient(args[3]), text) + elif op in ("MODE",): + forum = self.recipient(args[2]) + addl = args[3:] + elif op in ("JOIN", "PART"): + try: + forum = self.recipient(args[2]) + except IndexError: + forum = self.recipient(text) + elif op in ("QUIT",): + addl = (text,) + elif op in ("PING", "PONG"): + # PING ['PING'] us.boogernet.org. + # PONG ['irc.foonet.com', 'PONG', 'irc.foonet.com'] 1114199424 + addl = (text,) + elif op in ("NICK",): + # NICK ['brad!~brad@10.168.2.33', 'NICK'] bradaway + # + # The sender is the new nickname here, in case you want to + # send something to the sender. + + # Apparently there are two different standards for this + # command. + if text: + sender = self.recipient(text) + else: + sender = self.recipient(args[2]) + addl = (unpack_nuhost(args)[0],) + elif op in ("INVITE",): + # INVITE [u'pflarr!~pflarr@www.clanspum.net', u'INVITE', u'gallium', u'#mysterious'] + # INVITE [u'pflarr!~pflarr@www.clanspum.net', u'INVITE', u'gallium'] #mysterious + if len(args) > 3: + forum = self.recipient(args[3]) + else: + forum = self.recipient(text) + else: + try: + int(op) + except ValueError: + self.dbg("WARNING: unknown server code: %s" % op) + addl = tuple(args[2:]) + (text,) + + try: + self.handle_cooked(op, sender, forum, addl) + except SystemExit: + raise + except: + self.err(sys.exc_info()) + + def handle_cooked(self, op, sender, forum, addl): + try: + func = getattr(self, 'cmd_' + op.upper()) + except AttributeError: + self.unhandled(op, sender, forum, addl) + return + func(sender, forum, addl) + + def cmd_PING(self, sender, forum, addl): + self.write(['PONG'], addl[0]) + + def unhandled(self, op, sender, forum, addl): + """Handle all the stuff that had no handler. + + This is a special handler in that it also gets the server code + as the first argument. + + """ + + self.dbg("unhandled: %s" % ((op, sender, forum, addl),)) + + +class Bot(SmartIRCHandler): + """A simple bot. + + This automatically joins the channels you pass to the constructor, + tries to use one of the nicks provided, and reconnects if it gets + booted. You can use this as a base for more sophisticated bots. + + """ + + def __init__(self, host, nicks, gecos, channels): + self.nicks = nicks + self.channels = channels + self.waiting = True + self._spool = [] + SmartIRCHandler.__init__(self, host, nicks[0], gecos) + + def despool(self, target, lines): + """Slowly despool a bunch of lines to a target + + Since the IRC server will block all output if we send it too + fast, use this to send large multi-line responses. + + """ + + self._spool.append((target, list(lines))) + + def heartbeat(self): + SmartIRCHandler.heartbeat(self) + + # Despool data + if self._spool: + # Take the first one on the queue, and put it on the end + which = self._spool[0] + del self._spool[0] + self._spool.append(which) + + # Despool a line + target, lines = which + if lines: + line = lines[0] + target.msg(line) + del lines[0] + else: + self._spool.remove(which) + + def announce(self, text): + for c in self.channels: + self.write(['PRIVMSG', c], text) + + def err(self, exception): + SmartIRCHandler.err(self, exception) + self.announce('*bzert*') + + def cmd_001(self, sender, forum, addl): + # Welcome to IRC + self.nick = addl[0] + for c in self.channels: + self.write(['JOIN'], c) + + def cmd_433(self, sender, forum, addl): + # Nickname already in use + self.nicks.append(self.nicks.pop(0)) + self.write(['NICK', self.nicks[0]]) + + def cmd_NICK(self, sender, forum, addl): + if addl[0] == self.nick: + self.nick = sender.name() + print(self.nick) + + def writable(self): + if not self.waiting: + return asynchat.async_chat.writable(self) + else: + return False + + def write(self, *args): + SmartIRCHandler.write(self, *args) + + def close(self, final=False): + SmartIRCHandler.close(self) + if not final: + self.dbg("Connection closed, reconnecting...") + self.waiting = True + self.connected = 0 + # Wait a bit and reconnect + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.add_timer(23, lambda : self.connect(self.host)) + + def handle_close(self): + self.close() + + +## +## Miscellaneous IRC functions +## + +def unpack_nuhost(nuhost): + """Unpack nick!user@host + + Frequently, the first argument in a server message is in + nick!user@host format. You can just pass your whole argument list + to this function and get back a tuple containing: + + (nick, user, host) + + """ + + try: + [nick, uhost] = nuhost[0].split('!', 1) + [user, host] = uhost.split('@', 1) + except ValueError: + raise ValueError("not in nick!user@host format") + return (nick, user, host) + +def run_forever(timeout=2.0): + """Run your clients forever. + + Just a handy front-end to asyncore.loop, so you don't have to import + asyncore yourself. + + """ + + asyncore.loop(timeout) diff --git a/site-packages/png.py b/site-packages/png.py new file mode 100644 index 0000000..edf0750 --- /dev/null +++ b/site-packages/png.py @@ -0,0 +1,3720 @@ +#!/usr/bin/env python + +# $URL$ +# $Rev$ + +# png.py - PNG encoder/decoder in pure Python +# +# Copyright (C) 2006 Johann C. Rocholl +# Portions Copyright (C) 2009 David Jones +# And probably portions Copyright (C) 2006 Nicko van Someren +# +# Original concept by Johann C. Rocholl. +# +# LICENSE (The MIT License) +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Changelog (recent first): +# 2009-03-11 David: interlaced bit depth < 8 (writing). +# 2009-03-10 David: interlaced bit depth < 8 (reading). +# 2009-03-04 David: Flat and Boxed pixel formats. +# 2009-02-26 David: Palette support (writing). +# 2009-02-23 David: Bit-depths < 8; better PNM support. +# 2006-06-17 Nicko: Reworked into a class, faster interlacing. +# 2006-06-17 Johann: Very simple prototype PNG decoder. +# 2006-06-17 Nicko: Test suite with various image generators. +# 2006-06-17 Nicko: Alpha-channel, grey-scale, 16-bit/plane support. +# 2006-06-15 Johann: Scanline iterator interface for large input files. +# 2006-06-09 Johann: Very simple prototype PNG encoder. + +# Incorporated into Bangai-O Development Tools by drj on 2009-02-11 from +# http://trac.browsershots.org/browser/trunk/pypng/lib/png.py?rev=2885 + +# Incorporated into pypng by drj on 2009-03-12 from +# //depot/prj/bangaio/master/code/png.py#67 + + +""" +Pure Python PNG Reader/Writer + +This Python module implements support for PNG images (see PNG +specification at http://www.w3.org/TR/2003/REC-PNG-20031110/ ). It reads +and writes PNG files with all allowable bit depths (1/2/4/8/16/24/32/48/64 +bits per pixel) and colour combinations: greyscale (1/2/4/8/16 bit); RGB, +RGBA, LA (greyscale with alpha) with 8/16 bits per channel; colour mapped +images (1/2/4/8 bit). Adam7 interlacing is supported for reading and +writing. A number of optional chunks can be specified (when writing) +and understood (when reading): ``tRNS``, ``bKGD``, ``gAMA``. + +For help, type ``import png; help(png)`` in your python interpreter. + +A good place to start is the :class:`Reader` and :class:`Writer` classes. + +Requires Python 2.3. Limited support is available for Python 2.2, but +not everything works. Best with Python 2.4 and higher. Installation is +trivial, but see the ``README.txt`` file (with the source distribution) +for details. + +This file can also be used as a command-line utility to convert +`Netpbm `_ PNM files to PNG, and the reverse conversion from PNG to +PNM. The interface is similar to that of the ``pnmtopng`` program from +Netpbm. Type ``python png.py --help`` at the shell prompt +for usage and a list of options. + +A note on spelling and terminology +---------------------------------- + +Generally British English spelling is used in the documentation. So +that's "greyscale" and "colour". This not only matches the author's +native language, it's also used by the PNG specification. + +The major colour models supported by PNG (and hence by PyPNG) are: +greyscale, RGB, greyscale--alpha, RGB--alpha. These are sometimes +referred to using the abbreviations: L, RGB, LA, RGBA. In this case +each letter abbreviates a single channel: *L* is for Luminance or Luma or +Lightness which is the channel used in greyscale images; *R*, *G*, *B* stand +for Red, Green, Blue, the components of a colour image; *A* stands for +Alpha, the opacity channel (used for transparency effects, but higher +values are more opaque, so it makes sense to call it opacity). + +A note on formats +----------------- + +When getting pixel data out of this module (reading) and presenting +data to this module (writing) there are a number of ways the data could +be represented as a Python value. Generally this module uses one of +three formats called "flat row flat pixel", "boxed row flat pixel", and +"boxed row boxed pixel". Basically the concern is whether each pixel +and each row comes in its own little tuple (box), or not. + +Consider an image that is 3 pixels wide by 2 pixels high, and each pixel +has RGB components: + +Boxed row flat pixel:: + + list([R,G,B, R,G,B, R,G,B], + [R,G,B, R,G,B, R,G,B]) + +Each row appears as its own list, but the pixels are flattened so that +three values for one pixel simply follow the three values for the previous +pixel. This is the most common format used, because it provides a good +compromise between space and convenience. PyPNG regards itself as +at liberty to replace any sequence type with any sufficiently compatible +other sequence type; in practice each row is an array (from the array +module), and the outer list is sometimes an iterator rather than an +explicit list (so that streaming is possible). + +Flat row flat pixel:: + + [R,G,B, R,G,B, R,G,B, + R,G,B, R,G,B, R,G,B] + +The entire image is one single giant sequence of colour values. +Generally an array will be used (to save space), not a list. + +Boxed row boxed pixel:: + + list([ (R,G,B), (R,G,B), (R,G,B) ], + [ (R,G,B), (R,G,B), (R,G,B) ]) + +Each row appears in its own list, but each pixel also appears in its own +tuple. A serious memory burn in Python. + +In all cases the top row comes first, and for each row the pixels are +ordered from left-to-right. Within a pixel the values appear in the +order, R-G-B-A (or L-A for greyscale--alpha). + +There is a fourth format, mentioned because it is used internally, +is close to what lies inside a PNG file itself, and has some support +from the public API. This format is called packed. When packed, +each row is a sequence of bytes (integers from 0 to 255), just as +it is before PNG scanline filtering is applied. When the bit depth +is 8 this is essentially the same as boxed row flat pixel; when the +bit depth is less than 8, several pixels are packed into each byte; +when the bit depth is 16 (the only value more than 8 that is supported +by the PNG image format) each pixel value is decomposed into 2 bytes +(and `packed` is a misnomer). This format is used by the +:meth:`Writer.write_packed` method. It isn't usually a convenient +format, but may be just right if the source data for the PNG image +comes from something that uses a similar format (for example, 1-bit +BMPs, or another PNG file). + +And now, my famous members +-------------------------- +""" + +# http://www.python.org/doc/2.2.3/whatsnew/node5.html +from __future__ import generators + +__version__ = "$URL$ $Rev$" + +from array import array +try: # See :pyver:old + import itertools +except: + pass +import math +# http://www.python.org/doc/2.4.4/lib/module-operator.html +import operator +import struct +import sys +import zlib +# http://www.python.org/doc/2.4.4/lib/module-warnings.html +import warnings + + +__all__ = ['Image', 'Reader', 'Writer', 'write_chunks', 'from_array'] + + +# The PNG signature. +# http://www.w3.org/TR/PNG/#5PNG-file-signature +_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10) + +_adam7 = ((0, 0, 8, 8), + (4, 0, 8, 8), + (0, 4, 4, 8), + (2, 0, 4, 4), + (0, 2, 2, 4), + (1, 0, 2, 2), + (0, 1, 1, 2)) + +def group(s, n): + # See + # http://www.python.org/doc/2.6/library/functions.html#zip + return zip(*[iter(s)]*n) + +def isarray(x): + """Same as ``isinstance(x, array)`` except on Python 2.2, where it + always returns ``False``. This helps PyPNG work on Python 2.2. + """ + + try: + return isinstance(x, array) + except: + return False + +try: # see :pyver:old + array.tostring +except: + def tostring(row): + l = len(row) + return struct.pack('%dB' % l, *row) +else: + def tostring(row): + """Convert row of bytes to string. Expects `row` to be an + ``array``. + """ + return row.tostring() + + +def interleave_planes(ipixels, apixels, ipsize, apsize): + """ + Interleave (colour) planes, e.g. RGB + A = RGBA. + + Return an array of pixels consisting of the `ipsize` elements of data + from each pixel in `ipixels` followed by the `apsize` elements of data + from each pixel in `apixels`. Conventionally `ipixels` and + `apixels` are byte arrays so the sizes are bytes, but it actually + works with any arrays of the same type. The returned array is the + same type as the input arrays which should be the same type as each other. + """ + + itotal = len(ipixels) + atotal = len(apixels) + newtotal = itotal + atotal + newpsize = ipsize + apsize + # Set up the output buffer + # See http://www.python.org/doc/2.4.4/lib/module-array.html#l2h-1356 + out = array(ipixels.typecode) + # It's annoying that there is no cheap way to set the array size :-( + out.extend(ipixels) + out.extend(apixels) + # Interleave in the pixel data + for i in range(ipsize): + out[i:newtotal:newpsize] = ipixels[i:itotal:ipsize] + for i in range(apsize): + out[i+ipsize:newtotal:newpsize] = apixels[i:atotal:apsize] + return out + +def check_palette(palette): + """Check a palette argument (to the :class:`Writer` class) for validity. + Returns the palette as a list if okay; raises an exception otherwise. + """ + + # None is the default and is allowed. + if palette is None: + return None + + p = list(palette) + if not (0 < len(p) <= 256): + raise ValueError("a palette must have between 1 and 256 entries") + seen_triple = False + for i,t in enumerate(p): + if len(t) not in (3,4): + raise ValueError( + "palette entry %d: entries must be 3- or 4-tuples." % i) + if len(t) == 3: + seen_triple = True + if seen_triple and len(t) == 4: + raise ValueError( + "palette entry %d: all 4-tuples must precede all 3-tuples" % i) + for x in t: + if int(x) != x or not(0 <= x <= 255): + raise ValueError( + "palette entry %d: values must be integer: 0 <= x <= 255" % i) + return p + +class Error(Exception): + prefix = 'Error' + def __str__(self): + return self.prefix + ': ' + ' '.join(self.args) + +class FormatError(Error): + """Problem with input file format. In other words, PNG file does + not conform to the specification in some way and is invalid. + """ + + prefix = 'FormatError' + +class ChunkError(FormatError): + prefix = 'ChunkError' + + +class Writer: + """ + PNG encoder in pure Python. + """ + + def __init__(self, width=None, height=None, + size=None, + greyscale=False, + alpha=False, + bitdepth=8, + palette=None, + transparent=None, + background=None, + gamma=None, + compression=None, + interlace=False, + bytes_per_sample=None, # deprecated + planes=None, + colormap=None, + maxval=None, + chunk_limit=2**20): + """ + Create a PNG encoder object. + + Arguments: + + width, height + Image size in pixels, as two separate arguments. + size + Image size (w,h) in pixels, as single argument. + greyscale + Input data is greyscale, not RGB. + alpha + Input data has alpha channel (RGBA or LA). + bitdepth + Bit depth: from 1 to 16. + palette + Create a palette for a colour mapped image (colour type 3). + transparent + Specify a transparent colour (create a ``tRNS`` chunk). + background + Specify a default background colour (create a ``bKGD`` chunk). + gamma + Specify a gamma value (create a ``gAMA`` chunk). + compression + zlib compression level (1-9). + interlace + Create an interlaced image. + chunk_limit + Write multiple ``IDAT`` chunks to save memory. + + The image size (in pixels) can be specified either by using the + `width` and `height` arguments, or with the single `size` + argument. If `size` is used it should be a pair (*width*, + *height*). + + `greyscale` and `alpha` are booleans that specify whether + an image is greyscale (or colour), and whether it has an + alpha channel (or not). + + `bitdepth` specifies the bit depth of the source pixel values. + Each source pixel value must be an integer between 0 and + ``2**bitdepth-1``. For example, 8-bit images have values + between 0 and 255. PNG only stores images with bit depths of + 1,2,4,8, or 16. When `bitdepth` is not one of these values, + the next highest valid bit depth is selected, and an ``sBIT`` + (significant bits) chunk is generated that specifies the original + precision of the source image. In this case the supplied pixel + values will be rescaled to fit the range of the selected bit depth. + + The details of which bit depth / colour model combinations the + PNG file format supports directly, are somewhat arcane + (refer to the PNG specification for full details). Briefly: + "small" bit depths (1,2,4) are only allowed with greyscale and + colour mapped images; colour mapped images cannot have bit depth + 16. + + For colour mapped images (in other words, when the `palette` + argument is specified) the `bitdepth` argument must match one of + the valid PNG bit depths: 1, 2, 4, or 8. (It is valid to have a + PNG image with a palette and an ``sBIT`` chunk, but the meaning + is slightly different; it would be awkward to press the + `bitdepth` argument into service for this.) + + The `palette` option, when specified, causes a colour mapped image + to be created: the PNG colour type is set to 3; greyscale + must not be set; alpha must not be set; transparent must + not be set; the bit depth must be 1,2,4, or 8. When a colour + mapped image is created, the pixel values are palette indexes + and the `bitdepth` argument specifies the size of these indexes + (not the size of the colour values in the palette). + + The palette argument value should be a sequence of 3- or + 4-tuples. 3-tuples specify RGB palette entries; 4-tuples + specify RGBA palette entries. If both 4-tuples and 3-tuples + appear in the sequence then all the 4-tuples must come + before all the 3-tuples. A ``PLTE`` chunk is created; if there + are 4-tuples then a ``tRNS`` chunk is created as well. The + ``PLTE`` chunk will contain all the RGB triples in the same + sequence; the ``tRNS`` chunk will contain the alpha channel for + all the 4-tuples, in the same sequence. Palette entries + are always 8-bit. + + If specified, the `transparent` and `background` parameters must + be a tuple with three integer values for red, green, blue, or + a simple integer (or singleton tuple) for a greyscale image. + + If specified, the `gamma` parameter must be a positive number + (generally, a float). A ``gAMA`` chunk will be created. Note that + this will not change the values of the pixels as they appear in + the PNG file, they are assumed to have already been converted + appropriately for the gamma specified. + + The `compression` argument specifies the compression level + to be used by the ``zlib`` module. Higher values are likely + to compress better, but will be slower to compress. The + default for this argument is ``None``; this does not mean + no compression, rather it means that the default from the + ``zlib`` module is used (which is generally acceptable). + + If `interlace` is true then an interlaced image is created + (using PNG's so far only interace method, *Adam7*). This does not + affect how the pixels should be presented to the encoder, rather + it changes how they are arranged into the PNG file. On slow + connexions interlaced images can be partially decoded by the + browser to give a rough view of the image that is successively + refined as more image data appears. + + .. note :: + + Enabling the `interlace` option requires the entire image + to be processed in working memory. + + `chunk_limit` is used to limit the amount of memory used whilst + compressing the image. In order to avoid using large amounts of + memory, multiple ``IDAT`` chunks may be created. + """ + + # At the moment the `planes` argument is ignored; + # its purpose is to act as a dummy so that + # ``Writer(x, y, **info)`` works, where `info` is a dictionary + # returned by Reader.read and friends. + # Ditto for `colormap`. + + # A couple of helper functions come first. Best skipped if you + # are reading through. + + def isinteger(x): + try: + return int(x) == x + except: + return False + + def check_color(c, which): + """Checks that a colour argument for transparent or + background options is the right form. Also "corrects" bare + integers to 1-tuples. + """ + + if c is None: + return c + if greyscale: + try: + l = len(c) + except TypeError: + c = (c,) + if len(c) != 1: + raise ValueError("%s for greyscale must be 1-tuple" % + which) + if not isinteger(c[0]): + raise ValueError( + "%s colour for greyscale must be integer" % + which) + else: + if not (len(c) == 3 and + isinteger(c[0]) and + isinteger(c[1]) and + isinteger(c[2])): + raise ValueError( + "%s colour must be a triple of integers" % + which) + return c + + if size: + if len(size) != 2: + raise ValueError( + "size argument should be a pair (width, height)") + if width is not None and width != size[0]: + raise ValueError( + "size[0] (%r) and width (%r) should match when both are used." + % (size[0], width)) + if height is not None and height != size[1]: + raise ValueError( + "size[1] (%r) and height (%r) should match when both are used." + % (size[1], height)) + width,height = size + del size + + if width <= 0 or height <= 0: + raise ValueError("width and height must be greater than zero") + if not isinteger(width) or not isinteger(height): + raise ValueError("width and height must be integers") + # http://www.w3.org/TR/PNG/#7Integers-and-byte-order + if width > 2**32-1 or height > 2**32-1: + raise ValueError("width and height cannot exceed 2**32-1") + + if alpha and transparent is not None: + raise ValueError( + "transparent colour not allowed with alpha channel") + + if bytes_per_sample is not None: + warnings.warn('please use bitdepth instead of bytes_per_sample', + DeprecationWarning) + if bytes_per_sample not in (0.125, 0.25, 0.5, 1, 2): + raise ValueError( + "bytes per sample must be .125, .25, .5, 1, or 2") + bitdepth = int(8*bytes_per_sample) + del bytes_per_sample + if not isinteger(bitdepth) or bitdepth < 1 or 16 < bitdepth: + raise ValueError("bitdepth (%r) must be a postive integer <= 16" % + bitdepth) + + self.rescale = None + if palette: + if bitdepth not in (1,2,4,8): + raise ValueError("with palette, bitdepth must be 1, 2, 4, or 8") + if transparent is not None: + raise ValueError("transparent and palette not compatible") + if alpha: + raise ValueError("alpha and palette not compatible") + if greyscale: + raise ValueError("greyscale and palette not compatible") + else: + # No palette, check for sBIT chunk generation. + if alpha or not greyscale: + if bitdepth not in (8,16): + targetbitdepth = (8,16)[bitdepth > 8] + self.rescale = (bitdepth, targetbitdepth) + bitdepth = targetbitdepth + del targetbitdepth + else: + assert greyscale + assert not alpha + if bitdepth not in (1,2,4,8,16): + if bitdepth > 8: + targetbitdepth = 16 + elif bitdepth == 3: + targetbitdepth = 4 + else: + assert bitdepth in (5,6,7) + targetbitdepth = 8 + self.rescale = (bitdepth, targetbitdepth) + bitdepth = targetbitdepth + del targetbitdepth + + if bitdepth < 8 and (alpha or not greyscale and not palette): + raise ValueError( + "bitdepth < 8 only permitted with greyscale or palette") + if bitdepth > 8 and palette: + raise ValueError( + "bit depth must be 8 or less for images with palette") + + transparent = check_color(transparent, 'transparent') + background = check_color(background, 'background') + + # It's important that the true boolean values (greyscale, alpha, + # colormap, interlace) are converted to bool because Iverson's + # convention is relied upon later on. + self.width = width + self.height = height + self.transparent = transparent + self.background = background + self.gamma = gamma + self.greyscale = bool(greyscale) + self.alpha = bool(alpha) + self.colormap = bool(palette) + self.bitdepth = int(bitdepth) + self.compression = compression + self.chunk_limit = chunk_limit + self.interlace = bool(interlace) + self.palette = check_palette(palette) + + self.color_type = 4*self.alpha + 2*(not greyscale) + 1*self.colormap + assert self.color_type in (0,2,3,4,6) + + self.color_planes = (3,1)[self.greyscale or self.colormap] + self.planes = self.color_planes + self.alpha + # :todo: fix for bitdepth < 8 + self.psize = (self.bitdepth/8) * self.planes + + def make_palette(self): + """Create the byte sequences for a ``PLTE`` and if necessary a + ``tRNS`` chunk. Returned as a pair (*p*, *t*). *t* will be + ``None`` if no ``tRNS`` chunk is necessary. + """ + + p = array('B') + t = array('B') + + for x in self.palette: + p.extend(x[0:3]) + if len(x) > 3: + t.append(x[3]) + p = tostring(p) + t = tostring(t) + if t: + return p,t + return p,None + + def write(self, outfile, rows): + """Write a PNG image to the output file. `rows` should be + an iterable that yields each row in boxed row flat pixel format. + The rows should be the rows of the original image, so there + should be ``self.height`` rows of ``self.width * self.planes`` values. + If `interlace` is specified (when creating the instance), then + an interlaced PNG file will be written. Supply the rows in the + normal image order; the interlacing is carried out internally. + + .. note :: + + Interlacing will require the entire image to be in working memory. + """ + + if self.interlace: + fmt = 'BH'[self.bitdepth > 8] + a = array(fmt, itertools.chain(*rows)) + return self.write_array(outfile, a) + else: + nrows = self.write_passes(outfile, rows) + if nrows != self.height: + raise ValueError( + "rows supplied (%d) does not match height (%d)" % + (nrows, self.height)) + + def write_passes(self, outfile, rows, packed=False): + """ + Write a PNG image to the output file. + + Most users are expected to find the :meth:`write` or + :meth:`write_array` method more convenient. + + The rows should be given to this method in the order that + they appear in the output file. For straightlaced images, + this is the usual top to bottom ordering, but for interlaced + images the rows should have already been interlaced before + passing them to this function. + + `rows` should be an iterable that yields each row. When + `packed` is ``False`` the rows should be in boxed row flat pixel + format; when `packed` is ``True`` each row should be a packed + sequence of bytes. + + """ + + # http://www.w3.org/TR/PNG/#5PNG-file-signature + outfile.write(_signature) + + # http://www.w3.org/TR/PNG/#11IHDR + write_chunk(outfile, 'IHDR', + struct.pack("!2I5B", self.width, self.height, + self.bitdepth, self.color_type, + 0, 0, self.interlace)) + + # See :chunk:order + # http://www.w3.org/TR/PNG/#11gAMA + if self.gamma is not None: + write_chunk(outfile, 'gAMA', + struct.pack("!L", int(round(self.gamma*1e5)))) + + # See :chunk:order + # http://www.w3.org/TR/PNG/#11sBIT + if self.rescale: + write_chunk(outfile, 'sBIT', + struct.pack('%dB' % self.planes, + *[self.rescale[0]]*self.planes)) + + # :chunk:order: Without a palette (PLTE chunk), ordering is + # relatively relaxed. With one, gAMA chunk must precede PLTE + # chunk which must precede tRNS and bKGD. + # See http://www.w3.org/TR/PNG/#5ChunkOrdering + if self.palette: + p,t = self.make_palette() + write_chunk(outfile, 'PLTE', p) + if t: + # tRNS chunk is optional. Only needed if palette entries + # have alpha. + write_chunk(outfile, 'tRNS', t) + + # http://www.w3.org/TR/PNG/#11tRNS + if self.transparent is not None: + if self.greyscale: + write_chunk(outfile, 'tRNS', + struct.pack("!1H", *self.transparent)) + else: + write_chunk(outfile, 'tRNS', + struct.pack("!3H", *self.transparent)) + + # http://www.w3.org/TR/PNG/#11bKGD + if self.background is not None: + if self.greyscale: + write_chunk(outfile, 'bKGD', + struct.pack("!1H", *self.background)) + else: + write_chunk(outfile, 'bKGD', + struct.pack("!3H", *self.background)) + + # http://www.w3.org/TR/PNG/#11IDAT + if self.compression is not None: + compressor = zlib.compressobj(self.compression) + else: + compressor = zlib.compressobj() + + # Choose an extend function based on the bitdepth. The extend + # function packs/decomposes the pixel values into bytes and + # stuffs them onto the data array. + data = array('B') + if self.bitdepth == 8 or packed: + extend = data.extend + elif self.bitdepth == 16: + # Decompose into bytes + def extend(sl): + fmt = '!%dH' % len(sl) + data.extend(array('B', struct.pack(fmt, *sl))) + else: + # Pack into bytes + assert self.bitdepth < 8 + # samples per byte + spb = int(8/self.bitdepth) + def extend(sl): + a = array('B', sl) + # Adding padding bytes so we can group into a whole + # number of spb-tuples. + l = float(len(a)) + extra = math.ceil(l / float(spb))*spb - l + a.extend([0]*int(extra)) + # Pack into bytes + l = group(a, spb) + l = map(lambda e: reduce(lambda x,y: + (x << self.bitdepth) + y, e), l) + data.extend(l) + if self.rescale: + oldextend = extend + factor = \ + float(2**self.rescale[1]-1) / float(2**self.rescale[0]-1) + def extend(sl): + oldextend(map(lambda x: int(round(factor*x)), sl)) + + # Build the first row, testing mostly to see if we need to + # changed the extend function to cope with NumPy integer types + # (they cause our ordinary definition of extend to fail, so we + # wrap it). See + # http://code.google.com/p/pypng/issues/detail?id=44 + enumrows = enumerate(rows) + del rows + + # First row's filter type. + data.append(0) + # :todo: Certain exceptions in the call to ``.next()`` or the + # following try would indicate no row data supplied. + # Should catch. + i,row = enumrows.next() + try: + # If this fails... + extend(row) + except: + # ... try a version that converts the values to int first. + # Not only does this work for the (slightly broken) NumPy + # types, there are probably lots of other, unknown, "nearly" + # int types it works for. + def wrapmapint(f): + return lambda sl: f(map(int, sl)) + extend = wrapmapint(extend) + del wrapmapint + extend(row) + + for i,row in enumrows: + # Add "None" filter type. Currently, it's essential that + # this filter type be used for every scanline as we do not + # mark the first row of a reduced pass image; that means we + # could accidentally compute the wrong filtered scanline if + # we used "up", "average", or "paeth" on such a line. + data.append(0) + extend(row) + if len(data) > self.chunk_limit: + compressed = compressor.compress(tostring(data)) + if len(compressed): + # print >> sys.stderr, len(data), len(compressed) + write_chunk(outfile, 'IDAT', compressed) + # Because of our very witty definition of ``extend``, + # above, we must re-use the same ``data`` object. Hence + # we use ``del`` to empty this one, rather than create a + # fresh one (which would be my natural FP instinct). + del data[:] + if len(data): + compressed = compressor.compress(tostring(data)) + else: + compressed = '' + flushed = compressor.flush() + if len(compressed) or len(flushed): + # print >> sys.stderr, len(data), len(compressed), len(flushed) + write_chunk(outfile, 'IDAT', compressed + flushed) + # http://www.w3.org/TR/PNG/#11IEND + write_chunk(outfile, 'IEND') + return i+1 + + def write_array(self, outfile, pixels): + """ + Write an array in flat row flat pixel format as a PNG file on + the output file. See also :meth:`write` method. + """ + + if self.interlace: + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.array_scanlines(pixels)) + + def write_packed(self, outfile, rows): + """ + Write PNG file to `outfile`. The pixel data comes from `rows` + which should be in boxed row packed format. Each row should be + a sequence of packed bytes. + + Technically, this method does work for interlaced images but it + is best avoided. For interlaced images, the rows should be + presented in the order that they appear in the file. + + This method should not be used when the source image bit depth + is not one naturally supported by PNG; the bit depth should be + 1, 2, 4, 8, or 16. + """ + + if self.rescale: + raise Error("write_packed method not suitable for bit depth %d" % + self.rescale[0]) + return self.write_passes(outfile, rows, packed=True) + + def convert_pnm(self, infile, outfile): + """ + Convert a PNM file containing raw pixel data into a PNG file + with the parameters set in the writer object. Works for + (binary) PGM, PPM, and PAM formats. + """ + + if self.interlace: + pixels = array('B') + pixels.fromfile(infile, + (self.bitdepth/8) * self.color_planes * + self.width * self.height) + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.file_scanlines(infile)) + + def convert_ppm_and_pgm(self, ppmfile, pgmfile, outfile): + """ + Convert a PPM and PGM file containing raw pixel data into a + PNG outfile with the parameters set in the writer object. + """ + pixels = array('B') + pixels.fromfile(ppmfile, + (self.bitdepth/8) * self.color_planes * + self.width * self.height) + apixels = array('B') + apixels.fromfile(pgmfile, + (self.bitdepth/8) * + self.width * self.height) + pixels = interleave_planes(pixels, apixels, + (self.bitdepth/8) * self.color_planes, + (self.bitdepth/8)) + if self.interlace: + self.write_passes(outfile, self.array_scanlines_interlace(pixels)) + else: + self.write_passes(outfile, self.array_scanlines(pixels)) + + def file_scanlines(self, infile): + """ + Generates boxed rows in flat pixel format, from the input file + `infile`. It assumes that the input file is in a "Netpbm-like" + binary format, and is positioned at the beginning of the first + pixel. The number of pixels to read is taken from the image + dimensions (`width`, `height`, `planes`) and the number of bytes + per value is implied by the image `bitdepth`. + """ + + # Values per row + vpr = self.width * self.planes + row_bytes = vpr + if self.bitdepth > 8: + assert self.bitdepth == 16 + row_bytes *= 2 + fmt = '>%dH' % vpr + def line(): + return array('H', struct.unpack(fmt, infile.read(row_bytes))) + else: + def line(): + scanline = array('B', infile.read(row_bytes)) + return scanline + for y in range(self.height): + yield line() + + def array_scanlines(self, pixels): + """ + Generates boxed rows (flat pixels) from flat rows (flat pixels) + in an array. + """ + + # Values per row + vpr = self.width * self.planes + stop = 0 + for y in range(self.height): + start = stop + stop = start + vpr + yield pixels[start:stop] + + def array_scanlines_interlace(self, pixels): + """ + Generator for interlaced scanlines from an array. `pixels` is + the full source image in flat row flat pixel format. The + generator yields each scanline of the reduced passes in turn, in + boxed row flat pixel format. + """ + + # http://www.w3.org/TR/PNG/#8InterlaceMethods + # Array type. + fmt = 'BH'[self.bitdepth > 8] + # Value per row + vpr = self.width * self.planes + for xstart, ystart, xstep, ystep in _adam7: + if xstart >= self.width: + continue + # Pixels per row (of reduced image) + ppr = int(math.ceil((self.width-xstart)/float(xstep))) + # number of values in reduced image row. + row_len = ppr*self.planes + for y in range(ystart, self.height, ystep): + if xstep == 1: + offset = y * vpr + yield pixels[offset:offset+vpr] + else: + row = array(fmt) + # There's no easier way to set the length of an array + row.extend(pixels[0:row_len]) + offset = y * vpr + xstart * self.planes + end_offset = (y+1) * vpr + skip = self.planes * xstep + for i in range(self.planes): + row[i::self.planes] = \ + pixels[offset+i:end_offset:skip] + yield row + +def write_chunk(outfile, tag, data=''): + """ + Write a PNG chunk to the output file, including length and + checksum. + """ + + # http://www.w3.org/TR/PNG/#5Chunk-layout + outfile.write(struct.pack("!I", len(data))) + outfile.write(tag) + outfile.write(data) + checksum = zlib.crc32(tag) + checksum = zlib.crc32(data, checksum) + outfile.write(struct.pack("!i", checksum)) + +def write_chunks(out, chunks): + """Create a PNG file by writing out the chunks.""" + + out.write(_signature) + for chunk in chunks: + write_chunk(out, *chunk) + +def filter_scanline(type, line, fo, prev=None): + """Apply a scanline filter to a scanline. `type` specifies the + filter type (0 to 4); `line` specifies the current (unfiltered) + scanline as a sequence of bytes; `prev` specifies the previous + (unfiltered) scanline as a sequence of bytes. `fo` specifies the + filter offset; normally this is size of a pixel in bytes (the number + of bytes per sample times the number of channels), but when this is + < 1 (for bit depths < 8) then the filter offset is 1. + """ + + assert 0 <= type < 5 + + # The output array. Which, pathetically, we extend one-byte at a + # time (fortunately this is linear). + out = array('B', [type]) + + def sub(): + ai = -fo + for x in line: + if ai >= 0: + x = (x - line[ai]) & 0xff + out.append(x) + ai += 1 + def up(): + for i,x in enumerate(line): + x = (x - prev[i]) & 0xff + out.append(x) + def average(): + ai = -fo + for i,x in enumerate(line): + if ai >= 0: + x = (x - ((line[ai] + prev[i]) >> 1)) & 0xff + else: + x = (x - (prev[i] >> 1)) & 0xff + out.append(x) + ai += 1 + def paeth(): + # http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth + ai = -fo # also used for ci + for i,x in enumerate(line): + a = 0 + b = prev[i] + c = 0 + + if ai >= 0: + a = line[ai] + c = prev[ai] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: Pr = a + elif pb <= pc: Pr = b + else: Pr = c + + x = (x - Pr) & 0xff + out.append(x) + ai += 1 + + if not prev: + # We're on the first line. Some of the filters can be reduced + # to simpler cases which makes handling the line "off the top" + # of the image simpler. "up" becomes "none"; "paeth" becomes + # "left" (non-trivial, but true). "average" needs to be handled + # specially. + if type == 2: # "up" + return line # type = 0 + elif type == 3: + prev = [0]*len(line) + elif type == 4: # "paeth" + type = 1 + if type == 0: + out.extend(line) + elif type == 1: + sub() + elif type == 2: + up() + elif type == 3: + average() + else: # type == 4 + paeth() + return out + + +def from_array(a, mode=None, info={}): + """Create a PNG :class:`Image` object from a 2- or 3-dimensional array. + One application of this function is easy PIL-style saving: + ``png.from_array(pixels, 'L').save('foo.png')``. + + .. note : + + The use of the term *3-dimensional* is for marketing purposes + only. It doesn't actually work. Please bear with us. Meanwhile + enjoy the complimentary snacks (on request) and please use a + 2-dimensional array. + + Unless they are specified using the *info* parameter, the PNG's + height and width are taken from the array size. For a 3 dimensional + array the first axis is the height; the second axis is the width; + and the third axis is the channel number. Thus an RGB image that is + 16 pixels high and 8 wide will use an array that is 16x8x3. For 2 + dimensional arrays the first axis is the height, but the second axis + is ``width*channels``, so an RGB image that is 16 pixels high and 8 + wide will use a 2-dimensional array that is 16x24 (each row will be + 8*3==24 sample values). + + *mode* is a string that specifies the image colour format in a + PIL-style mode. It can be: + + ``'L'`` + greyscale (1 channel) + ``'LA'`` + greyscale with alpha (2 channel) + ``'RGB'`` + colour image (3 channel) + ``'RGBA'`` + colour image with alpha (4 channel) + + The mode string can also specify the bit depth (overriding how this + function normally derives the bit depth, see below). Appending + ``';16'`` to the mode will cause the PNG to be 16 bits per channel; + any decimal from 1 to 16 can be used to specify the bit depth. + + When a 2-dimensional array is used *mode* determines how many + channels the image has, and so allows the width to be derived from + the second array dimension. + + The array is expected to be a ``numpy`` array, but it can be any + suitable Python sequence. For example, a list of lists can be used: + ``png.from_array([[0, 255, 0], [255, 0, 255]], 'L')``. The exact + rules are: ``len(a)`` gives the first dimension, height; + ``len(a[0])`` gives the second dimension; ``len(a[0][0])`` gives the + third dimension, unless an exception is raised in which case a + 2-dimensional array is assumed. It's slightly more complicated than + that because an iterator of rows can be used, and it all still + works. Using an iterator allows data to be streamed efficiently. + + The bit depth of the PNG is normally taken from the array element's + datatype (but if *mode* specifies a bitdepth then that is used + instead). The array element's datatype is determined in a way which + is supposed to work both for ``numpy`` arrays and for Python + ``array.array`` objects. A 1 byte datatype will give a bit depth of + 8, a 2 byte datatype will give a bit depth of 16. If the datatype + does not have an implicit size, for example it is a plain Python + list of lists, as above, then a default of 8 is used. + + The *info* parameter is a dictionary that can be used to specify + metadata (in the same style as the arguments to the + :class:``png.Writer`` class). For this function the keys that are + useful are: + + height + overrides the height derived from the array dimensions and allows + *a* to be an iterable. + width + overrides the width derived from the array dimensions. + bitdepth + overrides the bit depth derived from the element datatype (but + must match *mode* if that also specifies a bit depth). + + Generally anything specified in the + *info* dictionary will override any implicit choices that this + function would otherwise make, but must match any explicit ones. + For example, if the *info* dictionary has a ``greyscale`` key then + this must be true when mode is ``'L'`` or ``'LA'`` and false when + mode is ``'RGB'`` or ``'RGBA'``. + """ + + # We abuse the *info* parameter by modifying it. Take a copy here. + # (Also typechecks *info* to some extent). + info = dict(info) + + # Syntax check mode string. + bitdepth = None + try: + mode = mode.split(';') + if len(mode) not in (1,2): + raise Error() + if mode[0] not in ('L', 'LA', 'RGB', 'RGBA'): + raise Error() + if len(mode) == 2: + try: + bitdepth = int(mode[1]) + except: + raise Error() + except Error: + raise Error("mode string should be 'RGB' or 'L;16' or similar.") + mode = mode[0] + + # Get bitdepth from *mode* if possible. + if bitdepth: + if info.get('bitdepth') and bitdepth != info['bitdepth']: + raise Error("mode bitdepth (%d) should match info bitdepth (%d)." % + (bitdepth, info['bitdepth'])) + info['bitdepth'] = bitdepth + + # Fill in and/or check entries in *info*. + # Dimensions. + if 'size' in info: + # Check width, height, size all match where used. + for dimension,axis in [('width', 0), ('height', 1)]: + if dimension in info: + if info[dimension] != info['size'][axis]: + raise Error( + "info[%r] shhould match info['size'][%r]." % + (dimension, axis)) + info['width'],info['height'] = info['size'] + if 'height' not in info: + try: + l = len(a) + except: + raise Error( + "len(a) does not work, supply info['height'] instead.") + info['height'] = l + # Colour format. + if 'greyscale' in info: + if bool(info['greyscale']) != ('L' in mode): + raise Error("info['greyscale'] should match mode.") + info['greyscale'] = 'L' in mode + if 'alpha' in info: + if bool(info['alpha']) != ('A' in mode): + raise Error("info['alpha'] should match mode.") + info['alpha'] = 'A' in mode + + planes = len(mode) + if 'planes' in info: + if info['planes'] != planes: + raise Error("info['planes'] should match mode.") + + # In order to work out whether we the array is 2D or 3D we need its + # first row, which requires that we take a copy of its iterator. + # We may also need the first row to derive width and bitdepth. + a,t = itertools.tee(a) + row = t.next() + del t + try: + row[0][0] + threed = True + testelement = row[0] + except: + threed = False + testelement = row + if 'width' not in info: + if threed: + width = len(row) + else: + width = len(row) // planes + info['width'] = width + + # Not implemented yet + assert not threed + + if 'bitdepth' not in info: + try: + dtype = testelement.dtype + # goto the "else:" clause. Sorry. + except: + try: + # Try a Python array.array. + bitdepth = 8 * testelement.itemsize + except: + # We can't determine it from the array element's + # datatype, use a default of 8. + bitdepth = 8 + else: + # If we got here without exception, we now assume that + # the array is a numpy array. + if dtype.kind == 'b': + bitdepth = 1 + else: + bitdepth = 8 * dtype.itemsize + info['bitdepth'] = bitdepth + + for thing in 'width height bitdepth greyscale alpha'.split(): + assert thing in info + return Image(a, info) + +# So that refugee's from PIL feel more at home. Not documented. +fromarray = from_array + +class Image: + """A PNG image. + You can create an :class:`Image` object from an array of pixels by calling + :meth:`png.from_array`. It can be saved to disk with the + :meth:`save` method.""" + def __init__(self, rows, info): + """ + .. note :: + + The constructor is not public. Please do not call it. + """ + + self.rows = rows + self.info = info + + def save(self, file): + """Save the image to *file*. If *file* looks like an open file + descriptor then it is used, otherwise it is treated as a + filename and a fresh file is opened. + + In general, you can only call this method once; after it has + been called the first time and the PNG image has been saved, the + source data will have been streamed, and cannot be streamed + again. + """ + + w = Writer(**self.info) + + try: + file.write + def close(): pass + except: + file = open(file, 'wb') + def close(): file.close() + + try: + w.write(file, self.rows) + finally: + close() + +class _readable: + """ + A simple file-like interface for strings and arrays. + """ + + def __init__(self, buf): + self.buf = buf + self.offset = 0 + + def read(self, n): + r = self.buf[self.offset:self.offset+n] + if isarray(r): + r = r.tostring() + self.offset += n + return r + + +class Reader: + """ + PNG decoder in pure Python. + """ + + def __init__(self, _guess=None, **kw): + """ + Create a PNG decoder object. + + The constructor expects exactly one keyword argument. If you + supply a positional argument instead, it will guess the input + type. You can choose among the following keyword arguments: + + filename + Name of input file (a PNG file). + file + A file-like object (object with a read() method). + bytes + ``array`` or ``string`` with PNG data. + + """ + if ((_guess is not None and len(kw) != 0) or + (_guess is None and len(kw) != 1)): + raise TypeError("Reader() takes exactly 1 argument") + + # Will be the first 8 bytes, later on. See validate_signature. + self.signature = None + self.transparent = None + # A pair of (len,type) if a chunk has been read but its data and + # checksum have not (in other words the file position is just + # past the 4 bytes that specify the chunk type). See preamble + # method for how this is used. + self.atchunk = None + + if _guess is not None: + if isarray(_guess): + kw["bytes"] = _guess + elif isinstance(_guess, str): + kw["filename"] = _guess + elif isinstance(_guess, file): + kw["file"] = _guess + + if "filename" in kw: + self.file = file(kw["filename"], "rb") + elif "file" in kw: + self.file = kw["file"] + elif "bytes" in kw: + self.file = _readable(kw["bytes"]) + else: + raise TypeError("expecting filename, file or bytes array") + + def chunk(self, seek=None): + """ + Read the next PNG chunk from the input file; returns a + (*type*,*data*) tuple. *type* is the chunk's type as a string + (all PNG chunk types are 4 characters long). *data* is the + chunk's data content, as a string. + + If the optional `seek` argument is + specified then it will keep reading chunks until it either runs + out of file or finds the type specified by the argument. Note + that in general the order of chunks in PNGs is unspecified, so + using `seek` can cause you to miss chunks. + """ + + self.validate_signature() + + while True: + # http://www.w3.org/TR/PNG/#5Chunk-layout + if not self.atchunk: + self.atchunk = self.chunklentype() + length,type = self.atchunk + self.atchunk = None + data = self.file.read(length) + if len(data) != length: + raise ChunkError('Chunk %s too short for required %i octets.' + % (type, length)) + checksum = self.file.read(4) + if len(checksum) != 4: + raise ValueError('Chunk %s too short for checksum.', tag) + if seek and type != seek: + continue + verify = zlib.crc32(type) + verify = zlib.crc32(data, verify) + # Whether the output from zlib.crc32 is signed or not varies + # according to hideous implementation details, see + # http://bugs.python.org/issue1202 . + # We coerce it to be positive here (in a way which works on + # Python 2.3 and older). + verify &= 2**32 - 1 + verify = struct.pack('!I', verify) + if checksum != verify: + # print repr(checksum) + (a, ) = struct.unpack('!I', checksum) + (b, ) = struct.unpack('!I', verify) + raise ChunkError( + "Checksum error in %s chunk: 0x%08X != 0x%08X." % + (type, a, b)) + return type, data + + def chunks(self): + """Return an iterator that will yield each chunk as a + (*chunktype*, *content*) pair. + """ + + while True: + t,v = self.chunk() + yield t,v + if t == 'IEND': + break + + def undo_filter(self, filter_type, scanline, previous): + """Undo the filter for a scanline. `scanline` is a sequence of + bytes that does not include the initial filter type byte. + `previous` is decoded previous scanline (for straightlaced + images this is the previous pixel row, but for interlaced + images, it is the previous scanline in the reduced image, which + in general is not the previous pixel row in the final image). + When there is no previous scanline (the first row of a + straightlaced image, or the first row in one of the passes in an + interlaced image), then this argument should be ``None``. + + The scanline will have the effects of filtering removed, and the + result will be returned as a fresh sequence of bytes. + """ + + # :todo: Would it be better to update scanline in place? + + # Create the result byte array. It seems that the best way to + # create the array to be the right size is to copy from an + # existing sequence. *sigh* + # If we fill the result with scanline, then this allows a + # micro-optimisation in the "null" and "sub" cases. + result = array('B', scanline) + + if filter_type == 0: + # And here, we _rely_ on filling the result with scanline, + # above. + return result + + if filter_type not in (1,2,3,4): + raise FormatError('Invalid PNG Filter Type.' + ' See http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters .') + + # Filter unit. The stride from one pixel to the corresponding + # byte from the previous previous. Normally this is the pixel + # size in bytes, but when this is smaller than 1, the previous + # byte is used instead. + fu = max(1, self.psize) + + # For the first line of a pass, synthesize a dummy previous + # line. An alternative approach would be to observe that on the + # first line 'up' is the same as 'null', 'paeth' is the same + # as 'sub', with only 'average' requiring any special case. + if not previous: + previous = array('B', [0]*len(scanline)) + + def sub(): + """Undo sub filter.""" + + ai = 0 + # Loops starts at index fu. Observe that the initial part + # of the result is already filled in correctly with + # scanline. + for i in range(fu, len(result)): + x = scanline[i] + a = result[ai] + result[i] = (x + a) & 0xff + ai += 1 + + def up(): + """Undo up filter.""" + + for i in range(len(result)): + x = scanline[i] + b = previous[i] + result[i] = (x + b) & 0xff + + def average(): + """Undo average filter.""" + + ai = -fu + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = 0 + else: + a = result[ai] + b = previous[i] + result[i] = (x + ((a + b) >> 1)) & 0xff + ai += 1 + + def paeth(): + """Undo Paeth filter.""" + + # Also used for ci. + ai = -fu + for i in range(len(result)): + x = scanline[i] + if ai < 0: + a = c = 0 + else: + a = result[ai] + c = previous[ai] + b = previous[i] + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + pr = a + elif pb <= pc: + pr = b + else: + pr = c + result[i] = (x + pr) & 0xff + ai += 1 + + # Call appropriate filter algorithm. Note that 0 has already + # been dealt with. + (None, sub, up, average, paeth)[filter_type]() + return result + + def deinterlace(self, raw): + """ + Read raw pixel data, undo filters, deinterlace, and flatten. + Return in flat row flat pixel format. + """ + + # print >> sys.stderr, ("Reading interlaced, w=%s, r=%s, planes=%s," + + # " bpp=%s") % (self.width, self.height, self.planes, self.bps) + # Values per row (of the target image) + vpr = self.width * self.planes + + # Make a result array, and make it big enough. Interleaving + # writes to the output array randomly (well, not quite), so the + # entire output array must be in memory. + fmt = 'BH'[self.bitdepth > 8] + a = array(fmt, [0]*vpr*self.height) + source_offset = 0 + + for xstart, ystart, xstep, ystep in _adam7: + # print >> sys.stderr, "Adam7: start=%s,%s step=%s,%s" % ( + # xstart, ystart, xstep, ystep) + if xstart >= self.width: + continue + # The previous (reconstructed) scanline. None at the + # beginning of a pass to indicate that there is no previous + # line. + recon = None + # Pixels per row (reduced pass image) + ppr = int(math.ceil((self.width-xstart)/float(xstep))) + # Row size in bytes for this pass. + row_size = int(math.ceil(self.psize * ppr)) + for y in range(ystart, self.height, ystep): + filter_type = raw[source_offset] + source_offset += 1 + scanline = raw[source_offset:source_offset+row_size] + source_offset += row_size + recon = self.undo_filter(filter_type, scanline, recon) + # Convert so that there is one element per pixel value + flat = self.serialtoflat(recon, ppr) + if xstep == 1: + assert xstart == 0 + offset = y * vpr + a[offset:offset+vpr] = flat + else: + offset = y * vpr + xstart * self.planes + end_offset = (y+1) * vpr + skip = self.planes * xstep + for i in range(self.planes): + a[offset+i:end_offset:skip] = \ + flat[i::self.planes] + return a + + def iterboxed(self, rows): + """Iterator that yields each scanline in boxed row flat pixel + format. `rows` should be an iterator that yields the bytes of + each row in turn. + """ + + def asvalues(raw): + """Convert a row of raw bytes into a flat row. Result may + or may not share with argument""" + + if self.bitdepth == 8: + return raw + if self.bitdepth == 16: + raw = tostring(raw) + return array('H', struct.unpack('!%dH' % (len(raw)//2), raw)) + assert self.bitdepth < 8 + width = self.width + # Samples per byte + spb = 8//self.bitdepth + out = array('B') + mask = 2**self.bitdepth - 1 + shifts = map(self.bitdepth.__mul__, reversed(range(spb))) + for o in raw: + out.extend(map(lambda i: mask&(o>>i), shifts)) + return out[:width] + + return itertools.imap(asvalues, rows) + + def serialtoflat(self, bytes, width=None): + """Convert serial format (byte stream) pixel data to flat row + flat pixel. + """ + + if self.bitdepth == 8: + return bytes + if self.bitdepth == 16: + bytes = tostring(bytes) + return array('H', + struct.unpack('!%dH' % (len(bytes)//2), bytes)) + assert self.bitdepth < 8 + if width is None: + width = self.width + # Samples per byte + spb = 8//self.bitdepth + out = array('B') + mask = 2**self.bitdepth - 1 + shifts = map(self.bitdepth.__mul__, reversed(range(spb))) + l = width + for o in bytes: + out.extend(map(lambda i: mask&(o>>i), shifts)[:l]) + l -= spb + if l <= 0: + l = width + return out + + def iterstraight(self, raw): + """Iterator that undoes the effect of filtering, and yields each + row in serialised format (as a sequence of bytes). Assumes input + is straightlaced. `raw` should be an iterable that yields the + raw bytes in chunks of arbitrary size.""" + + # length of row, in bytes + rb = self.row_bytes + a = array('B') + # The previous (reconstructed) scanline. None indicates first + # line of image. + recon = None + for some in raw: + a.extend(some) + while len(a) >= rb + 1: + filter_type = a[0] + scanline = a[1:rb+1] + del a[:rb+1] + recon = self.undo_filter(filter_type, scanline, recon) + yield recon + if len(a) != 0: + # :file:format We get here with a file format error: when the + # available bytes (after decompressing) do not pack into exact + # rows. + raise FormatError( + 'Wrong size for decompressed IDAT chunk.') + assert len(a) == 0 + + def validate_signature(self): + """If signature (header) has not been read then read and + validate it; otherwise do nothing. + """ + + if self.signature: + return + self.signature = self.file.read(8) + if self.signature != _signature: + raise FormatError("PNG file has invalid signature.") + + def preamble(self): + """ + Extract the image metadata by reading the initial part of the PNG + file up to the start of the ``IDAT`` chunk. All the chunks that + precede the ``IDAT`` chunk are read and either processed for + metadata or discarded. + """ + + self.validate_signature() + + while True: + if not self.atchunk: + self.atchunk = self.chunklentype() + if self.atchunk is None: + raise FormatError( + 'This PNG file has no IDAT chunks.') + if self.atchunk[1] == 'IDAT': + return + self.process_chunk() + + def chunklentype(self): + """Reads just enough of the input to determine the next + chunk's length and type, returned as a (*length*, *type*) pair + where *type* is a string. If there are no more chunks, ``None`` + is returned. + """ + + x = self.file.read(8) + if not x: + return None + if len(x) != 8: + raise FormatError( + 'End of file whilst reading chunk length and type.') + length,type = struct.unpack('!I4s', x) + if length > 2**31-1: + raise FormatError('Chunk %s is too large: %d.' % (type,length)) + return length,type + + def process_chunk(self): + """Process the next chunk and its data. This only processes the + following chunk types, all others are ignored: ``IHDR``, + ``PLTE``, ``bKGD``, ``tRNS``, ``gAMA``, ``sBIT``. + """ + + type, data = self.chunk() + if type == 'IHDR': + # http://www.w3.org/TR/PNG/#11IHDR + if len(data) != 13: + raise FormatError('IHDR chunk has incorrect length.') + (self.width, self.height, self.bitdepth, self.color_type, + self.compression, self.filter, + self.interlace) = struct.unpack("!2I5B", data) + + # Check that the header specifies only valid combinations. + if self.bitdepth not in (1,2,4,8,16): + raise Error("invalid bit depth %d" % self.bitdepth) + if self.color_type not in (0,2,3,4,6): + raise Error("invalid colour type %d" % self.color_type) + # Check indexed (palettized) images have 8 or fewer bits + # per pixel; check only indexed or greyscale images have + # fewer than 8 bits per pixel. + if ((self.color_type & 1 and self.bitdepth > 8) or + (self.bitdepth < 8 and self.color_type not in (0,3))): + raise FormatError("Illegal combination of bit depth (%d)" + " and colour type (%d)." + " See http://www.w3.org/TR/2003/REC-PNG-20031110/#table111 ." + % (self.bitdepth, self.color_type)) + if self.compression != 0: + raise Error("unknown compression method %d" % self.compression) + if self.filter != 0: + raise FormatError("Unknown filter method %d," + " see http://www.w3.org/TR/2003/REC-PNG-20031110/#9Filters ." + % self.filter) + if self.interlace not in (0,1): + raise FormatError("Unknown interlace method %d," + " see http://www.w3.org/TR/2003/REC-PNG-20031110/#8InterlaceMethods ." + % self.interlace) + + # Derived values + # http://www.w3.org/TR/PNG/#6Colour-values + colormap = bool(self.color_type & 1) + greyscale = not (self.color_type & 2) + alpha = bool(self.color_type & 4) + color_planes = (3,1)[greyscale or colormap] + planes = color_planes + alpha + + self.colormap = colormap + self.greyscale = greyscale + self.alpha = alpha + self.color_planes = color_planes + self.planes = planes + self.psize = float(self.bitdepth)/float(8) * planes + if int(self.psize) == self.psize: + self.psize = int(self.psize) + self.row_bytes = int(math.ceil(self.width * self.psize)) + # Stores PLTE chunk if present, and is used to check + # chunk ordering constraints. + self.plte = None + # Stores tRNS chunk if present, and is used to check chunk + # ordering constraints. + self.trns = None + # Stores sbit chunk if present. + self.sbit = None + elif type == 'PLTE': + # http://www.w3.org/TR/PNG/#11PLTE + if self.plte: + warnings.warn("Multiple PLTE chunks present.") + self.plte = data + if len(data) % 3 != 0: + raise FormatError( + "PLTE chunk's length should be a multiple of 3.") + if len(data) > (2**self.bitdepth)*3: + raise FormatError("PLTE chunk is too long.") + if len(data) == 0: + raise FormatError("Empty PLTE is not allowed.") + elif type == 'bKGD': + try: + if self.colormap: + if not self.plte: + warnings.warn( + "PLTE chunk is required before bKGD chunk.") + self.background = struct.unpack('B', data) + else: + self.background = struct.unpack("!%dH" % self.color_planes, + data) + except struct.error: + raise FormatError("bKGD chunk has incorrect length.") + elif type == 'tRNS': + # http://www.w3.org/TR/PNG/#11tRNS + self.trns = data + if self.colormap: + if not self.plte: + warnings.warn("PLTE chunk is required before tRNS chunk.") + else: + if len(data) > len(self.plte)/3: + # Was warning, but promoted to Error as it + # would otherwise cause pain later on. + raise FormatError("tRNS chunk is too long.") + else: + if self.alpha: + raise FormatError( + "tRNS chunk is not valid with colour type %d." % + self.color_type) + try: + self.transparent = \ + struct.unpack("!%dH" % self.color_planes, data) + except struct.error: + raise FormatError("tRNS chunk has incorrect length.") + elif type == 'gAMA': + try: + self.gamma = struct.unpack("!L", data)[0] / 100000.0 + except struct.error: + raise FormatError("gAMA chunk has incorrect length.") + elif type == 'sBIT': + self.sbit = data + if (self.colormap and len(data) != 3 or + not self.colormap and len(data) != self.planes): + raise FormatError("sBIT chunk has incorrect length.") + + def read(self): + """ + Read the PNG file and decode it. Returns (`width`, `height`, + `pixels`, `metadata`). + + May use excessive memory. + + `pixels` are returned in boxed row flat pixel format. + """ + + def iteridat(): + """Iterator that yields all the ``IDAT`` chunks as strings.""" + while True: + try: + type, data = self.chunk() + except ValueError, e: + raise ChunkError(e.args[0]) + if type == 'IEND': + # http://www.w3.org/TR/PNG/#11IEND + break + if type != 'IDAT': + continue + # type == 'IDAT' + # http://www.w3.org/TR/PNG/#11IDAT + if self.colormap and not self.plte: + warnings.warn("PLTE chunk is required before IDAT chunk") + yield data + + def iterdecomp(idat): + """Iterator that yields decompressed strings. `idat` should + be an iterator that yields the ``IDAT`` chunk data. + """ + + # Currently, with no max_length paramter to decompress, this + # routine will do one yield per IDAT chunk. So not very + # incremental. + d = zlib.decompressobj() + # The decompression loop: + # Decompress an IDAT chunk, then decompress any remaining + # unused data until the unused data does not get any + # smaller. Add the unused data to the front of the input + # and loop to process the next IDAT chunk. + cdata = '' + for data in idat: + # :todo: add a max_length argument here to limit output + # size. + yield array('B', d.decompress(cdata + data)) + yield array('B', d.flush()) + + self.preamble() + raw = iterdecomp(iteridat()) + + if self.interlace: + raw = array('B', itertools.chain(*raw)) + arraycode = 'BH'[self.bitdepth>8] + # Like :meth:`group` but producing an array.array object for + # each row. + pixels = itertools.imap(lambda *row: array(arraycode, row), + *[iter(self.deinterlace(raw))]*self.width*self.planes) + else: + pixels = self.iterboxed(self.iterstraight(raw)) + meta = dict() + for attr in 'greyscale alpha planes bitdepth interlace'.split(): + meta[attr] = getattr(self, attr) + meta['size'] = (self.width, self.height) + for attr in 'gamma transparent background'.split(): + a = getattr(self, attr, None) + if a is not None: + meta[attr] = a + return self.width, self.height, pixels, meta + + + def read_flat(self): + """ + Read a PNG file and decode it into flat row flat pixel format. + Returns (*width*, *height*, *pixels*, *metadata*). + + May use excessive memory. + + `pixels` are returned in flat row flat pixel format. + + See also the :meth:`read` method which returns pixels in the + more stream-friendly boxed row flat pixel format. + """ + + x, y, pixel, meta = self.read() + arraycode = 'BH'[meta['bitdepth']>8] + pixel = array(arraycode, itertools.chain(*pixel)) + return x, y, pixel, meta + + def palette(self, alpha='natural'): + """Returns a palette that is a sequence of 3-tuples or 4-tuples, + synthesizing it from the ``PLTE`` and ``tRNS`` chunks. These + chunks should have already been processed (for example, by + calling the :meth:`preamble` method). All the tuples are the + same size: 3-tuples if there is no ``tRNS`` chunk, 4-tuples when + there is a ``tRNS`` chunk. Assumes that the image is colour type + 3 and therefore a ``PLTE`` chunk is required. + + If the `alpha` argument is ``'force'`` then an alpha channel is + always added, forcing the result to be a sequence of 4-tuples. + """ + + if not self.plte: + raise FormatError( + "Required PLTE chunk is missing in colour type 3 image.") + plte = group(array('B', self.plte), 3) + if self.trns or alpha == 'force': + trns = array('B', self.trns or '') + trns.extend([255]*(len(plte)-len(trns))) + plte = map(operator.add, plte, group(trns, 1)) + return plte + + def asDirect(self): + """Returns the image data as a direct representation of an + ``x * y * planes`` array. This method is intended to remove the + need for callers to deal with palettes and transparency + themselves. Images with a palette (colour type 3) + are converted to RGB or RGBA; images with transparency (a + ``tRNS`` chunk) are converted to LA or RGBA as appropriate. + When returned in this format the pixel values represent the + colour value directly without needing to refer to palettes or + transparency information. + + Like the :meth:`read` method this method returns a 4-tuple: + + (*width*, *height*, *pixels*, *meta*) + + This method normally returns pixel values with the bit depth + they have in the source image, but when the source PNG has an + ``sBIT`` chunk it is inspected and can reduce the bit depth of + the result pixels; pixel values will be reduced according to + the bit depth specified in the ``sBIT`` chunk (PNG nerds should + note a single result bit depth is used for all channels; the + maximum of the ones specified in the ``sBIT`` chunk. An RGB565 + image will be rescaled to 6-bit RGB666). + + The *meta* dictionary that is returned reflects the `direct` + format and not the original source image. For example, an RGB + source image with a ``tRNS`` chunk to represent a transparent + colour, will have ``planes=3`` and ``alpha=False`` for the + source image, but the *meta* dictionary returned by this method + will have ``planes=4`` and ``alpha=True`` because an alpha + channel is synthesized and added. + + *pixels* is the pixel data in boxed row flat pixel format (just + like the :meth:`read` method). + + All the other aspects of the image data are not changed. + """ + + self.preamble() + + # Simple case, no conversion necessary. + if not self.colormap and not self.trns and not self.sbit: + return self.read() + + x,y,pixels,meta = self.read() + + if self.colormap: + meta['colormap'] = False + meta['alpha'] = bool(self.trns) + meta['bitdepth'] = 8 + meta['planes'] = 3 + bool(self.trns) + plte = self.palette() + def iterpal(pixels): + for row in pixels: + row = map(plte.__getitem__, row) + yield array('B', itertools.chain(*row)) + pixels = iterpal(pixels) + elif self.trns: + # It would be nice if there was some reasonable way of doing + # this without generating a whole load of intermediate tuples. + # But tuples does seem like the easiest way, with no other way + # clearly much simpler or much faster. (Actually, the L to LA + # conversion could perhaps go faster (all those 1-tuples!), but + # I still wonder whether the code proliferation is worth it) + it = self.transparent + maxval = 2**meta['bitdepth']-1 + planes = meta['planes'] + meta['alpha'] = True + meta['planes'] += 1 + typecode = 'BH'[meta['bitdepth']>8] + def itertrns(pixels): + for row in pixels: + # For each row we group it into pixels, then form a + # characterisation vector that says whether each pixel + # is opaque or not. Then we convert True/False to + # 0/maxval (by multiplication), and add it as the extra + # channel. + row = group(row, planes) + opa = map(it.__ne__, row) + opa = map(maxval.__mul__, opa) + opa = zip(opa) # convert to 1-tuples + yield array(typecode, + itertools.chain(*map(operator.add, row, opa))) + pixels = itertrns(pixels) + targetbitdepth = None + if self.sbit: + sbit = struct.unpack('%dB' % len(self.sbit), self.sbit) + targetbitdepth = max(sbit) + if targetbitdepth > meta['bitdepth']: + raise Error('sBIT chunk %r exceeds bitdepth %d' % + (sbit,self.bitdepth)) + if min(sbit) <= 0: + raise Error('sBIT chunk %r has a 0-entry' % sbit) + if targetbitdepth == meta['bitdepth']: + targetbitdepth = None + if targetbitdepth: + shift = meta['bitdepth'] - targetbitdepth + meta['bitdepth'] = targetbitdepth + def itershift(pixels): + for row in pixels: + yield map(shift.__rrshift__, row) + pixels = itershift(pixels) + return x,y,pixels,meta + + def asFloat(self, maxval=1.0): + """Return image pixels as per :meth:`asDirect` method, but scale + all pixel values to be floating point values between 0.0 and + *maxval*. + """ + + x,y,pixels,info = self.asDirect() + sourcemaxval = 2**info['bitdepth']-1 + del info['bitdepth'] + info['maxval'] = float(maxval) + factor = float(maxval)/float(sourcemaxval) + def iterfloat(): + for row in pixels: + yield map(factor.__mul__, row) + return x,y,iterfloat(),info + + def _as_rescale(self, get, targetbitdepth): + """Helper used by :meth:`asRGB8` and :meth:`asRGBA8`.""" + + width,height,pixels,meta = get() + maxval = 2**meta['bitdepth'] - 1 + targetmaxval = 2**targetbitdepth - 1 + factor = float(targetmaxval) / float(maxval) + meta['bitdepth'] = targetbitdepth + def iterscale(): + for row in pixels: + yield map(lambda x: int(round(x*factor)), row) + return width, height, iterscale(), meta + + def asRGB8(self): + """Return the image data as an RGB pixels with 8-bits per + sample. This is like the :meth:`asRGB` method except that + this method additionally rescales the values so that they + are all between 0 and 255 (8-bit). In the case where the + source image has a bit depth < 8 the transformation preserves + all the information; where the source image has bit depth + > 8, then rescaling to 8-bit values loses precision. No + dithering is performed. Like :meth:`asRGB`, an alpha channel + in the source image will raise an exception. + + This function returns a 4-tuple: + (*width*, *height*, *pixels*, *metadata*). + *width*, *height*, *metadata* are as per the :meth:`read` method. + + *pixels* is the pixel data in boxed row flat pixel format. + """ + + return self._as_rescale(self.asRGB, 8) + + def asRGBA8(self): + """Return the image data as RGBA pixels with 8-bits per + sample. This method is similar to :meth:`asRGB8` and + :meth:`asRGBA`: The result pixels have an alpha channel, *and* + values are rescaled to the range 0 to 255. The alpha channel is + synthesized if necessary (with a small speed penalty). + """ + + return self._as_rescale(self.asRGBA, 8) + + def asRGB(self): + """Return image as RGB pixels. RGB colour images are passed + through unchanged; greyscales are expanded into RGB + triplets (there is a small speed overhead for doing this). + + An alpha channel in the source image will raise an + exception. + + The return values are as for the :meth:`read` method + except that the *metadata* reflect the returned pixels, not the + source image. In particular, for this method + ``metadata['greyscale']`` will be ``False``. + """ + + width,height,pixels,meta = self.asDirect() + if meta['alpha']: + raise Error("will not convert image with alpha channel to RGB") + if not meta['greyscale']: + return width,height,pixels,meta + meta['greyscale'] = False + typecode = 'BH'[meta['bitdepth'] > 8] + def iterrgb(): + for row in pixels: + a = array(typecode, [0]) * 3 * width + for i in range(3): + a[i::3] = row + yield a + return width,height,iterrgb(),meta + + def asRGBA(self): + """Return image as RGBA pixels. Greyscales are expanded into + RGB triplets; an alpha channel is synthesized if necessary. + The return values are as for the :meth:`read` method + except that the *metadata* reflect the returned pixels, not the + source image. In particular, for this method + ``metadata['greyscale']`` will be ``False``, and + ``metadata['alpha']`` will be ``True``. + """ + + width,height,pixels,meta = self.asDirect() + if meta['alpha'] and not meta['greyscale']: + return width,height,pixels,meta + typecode = 'BH'[meta['bitdepth'] > 8] + maxval = 2**meta['bitdepth'] - 1 + def newarray(): + return array(typecode, [0]) * 4 * width + if meta['alpha'] and meta['greyscale']: + # LA to RGBA + def convert(): + for row in pixels: + # Create a fresh target row, then copy L channel + # into first three target channels, and A channel + # into fourth channel. + a = newarray() + for i in range(3): + a[i::4] = row[0::2] + a[3::4] = row[1::2] + yield a + elif meta['greyscale']: + # L to RGBA + def convert(): + for row in pixels: + a = newarray() + for i in range(3): + a[i::4] = row + a[3::4] = array(typecode, maxval) * width + yield a + else: + assert not meta['alpha'] and not meta['greyscale'] + # RGB to RGBA + def convert(): + for row in pixels: + a = newarray() + for i in range(3): + a[i::4] = row[i::3] + a[3::4] = array(typecode, [maxval]) * width + yield a + meta['alpha'] = True + meta['greyscale'] = False + return width,height,convert(),meta + + +# === Legacy Version Support === + +# :pyver:old: PyPNG works on Python versions 2.3 and 2.2, but not +# without some awkward problems. Really PyPNG works on Python 2.4 (and +# above); it works on Pythons 2.3 and 2.2 by virtue of fixing up +# problems here. It's a bit ugly (which is why it's hidden down here). +# +# Generally the strategy is one of pretending that we're running on +# Python 2.4 (or above), and patching up the library support on earlier +# versions so that it looks enough like Python 2.4. When it comes to +# Python 2.2 there is one thing we cannot patch: extended slices +# http://www.python.org/doc/2.3/whatsnew/section-slices.html. +# Instead we simply declare that features that are implemented using +# extended slices will not work on Python 2.2. +# +# In order to work on Python 2.3 we fix up a recurring annoyance involving +# the array type. In Python 2.3 an array cannot be initialised with an +# array, and it cannot be extended with a list (or other sequence). +# Both of those are repeated issues in the code. Whilst I would not +# normally tolerate this sort of behaviour, here we "shim" a replacement +# for array into place (and hope no-ones notices). You never read this. +# +# In an amusing case of warty hacks on top of warty hacks... the array +# shimming we try and do only works on Python 2.3 and above (you can't +# subclass array.array in Python 2.2). So to get it working on Python +# 2.2 we go for something much simpler and (probably) way slower. +try: + array('B').extend([]) + array('B', array('B')) +except: + # Expect to get here on Python 2.3 + try: + class _array_shim(array): + true_array = array + def __new__(cls, typecode, init=None): + super_new = super(_array_shim, cls).__new__ + it = super_new(cls, typecode) + if init is None: + return it + it.extend(init) + return it + def extend(self, extension): + super_extend = super(_array_shim, self).extend + if isinstance(extension, self.true_array): + return super_extend(extension) + if not isinstance(extension, (list, str)): + # Convert to list. Allows iterators to work. + extension = list(extension) + return super_extend(self.true_array(self.typecode, extension)) + array = _array_shim + except: + # Expect to get here on Python 2.2 + def array(typecode, init=()): + if type(init) == str: + return map(ord, init) + return list(init) + +# Further hacks to get it limping along on Python 2.2 +try: + enumerate +except: + def enumerate(seq): + i=0 + for x in seq: + yield i,x + i += 1 + +try: + reversed +except: + def reversed(l): + l = list(l) + l.reverse() + for x in l: + yield x + +try: + itertools +except: + class _dummy_itertools: + pass + itertools = _dummy_itertools() + def _itertools_imap(f, seq): + for x in seq: + yield f(x) + itertools.imap = _itertools_imap + def _itertools_chain(*iterables): + for it in iterables: + for element in it: + yield element + itertools.chain = _itertools_chain + + + +# === Internal Test Support === + +# This section comprises the tests that are internally validated (as +# opposed to tests which produce output files that are externally +# validated). Primarily they are unittests. + +# Note that it is difficult to internally validate the results of +# writing a PNG file. The only thing we can do is read it back in +# again, which merely checks consistency, not that the PNG file we +# produce is valid. + +# Run the tests from the command line: +# python -c 'import png;png.test()' + +from StringIO import StringIO +import tempfile +# http://www.python.org/doc/2.4.4/lib/module-unittest.html +import unittest + + +def test(): + unittest.main(__name__) + +def topngbytes(name, rows, x, y, **k): + """Convenience function for creating a PNG file "in memory" as a + string. Creates a :class:`Writer` instance using the keyword arguments, + then passes `rows` to its :meth:`Writer.write` method. The resulting + PNG file is returned as a string. `name` is used to identify the file for + debugging. + """ + + import os + + print name + f = StringIO() + w = Writer(x, y, **k) + w.write(f, rows) + if os.environ.get('PYPNG_TEST_TMP'): + w = open(name, 'wb') + w.write(f.getvalue()) + w.close() + return f.getvalue() + +def testWithIO(inp, out, f): + """Calls the function `f` with ``sys.stdin`` changed to `inp` + and ``sys.stdout`` changed to `out`. They are restored when `f` + returns. This function returns whatever `f` returns. + """ + try: + oldin,sys.stdin = sys.stdin,inp + oldout,sys.stdout = sys.stdout,out + x = f() + finally: + sys.stdin = oldin + sys.stdout = oldout + return x + +class Test(unittest.TestCase): + # This member is used by the superclass. If we don't define a new + # class here then when we use self.assertRaises() and the PyPNG code + # raises an assertion then we get no proper traceback. I can't work + # out why, but defining a new class here means we get a proper + # traceback. + class failureException(Exception): + pass + + def helperLN(self, n): + mask = (1 << n) - 1 + # Use small chunk_limit so that multiple chunk writing is + # tested. Making it a test for Issue 20. + w = Writer(15, 17, greyscale=True, bitdepth=n, chunk_limit=99) + f = StringIO() + w.write_array(f, array('B', map(mask.__and__, range(1, 256)))) + r = Reader(bytes=f.getvalue()) + x,y,pixels,meta = r.read() + self.assertEqual(x, 15) + self.assertEqual(y, 17) + self.assertEqual(list(itertools.chain(*pixels)), + map(mask.__and__, range(1,256))) + def testL8(self): + return self.helperLN(8) + def testL4(self): + return self.helperLN(4) + def testL2(self): + "Also tests asRGB8." + w = Writer(1, 4, greyscale=True, bitdepth=2) + f = StringIO() + w.write_array(f, array('B', range(4))) + r = Reader(bytes=f.getvalue()) + x,y,pixels,meta = r.asRGB8() + self.assertEqual(x, 1) + self.assertEqual(y, 4) + for i,row in enumerate(pixels): + self.assertEqual(len(row), 3) + self.assertEqual(list(row), [0x55*i]*3) + def testP2(self): + "2-bit palette." + a = (255,255,255) + b = (200,120,120) + c = (50,99,50) + w = Writer(1, 4, bitdepth=2, palette=[a,b,c]) + f = StringIO() + w.write_array(f, array('B', (0,1,1,2))) + r = Reader(bytes=f.getvalue()) + x,y,pixels,meta = r.asRGB8() + self.assertEqual(x, 1) + self.assertEqual(y, 4) + self.assertEqual(list(pixels), map(list, [a, b, b, c])) + def testPtrns(self): + "Test colour type 3 and tRNS chunk (and 4-bit palette)." + a = (50,99,50,50) + b = (200,120,120,80) + c = (255,255,255) + d = (200,120,120) + e = (50,99,50) + w = Writer(3, 3, bitdepth=4, palette=[a,b,c,d,e]) + f = StringIO() + w.write_array(f, array('B', (4, 3, 2, 3, 2, 0, 2, 0, 1))) + r = Reader(bytes=f.getvalue()) + x,y,pixels,meta = r.asRGBA8() + self.assertEquals(x, 3) + self.assertEquals(y, 3) + c = c+(255,) + d = d+(255,) + e = e+(255,) + boxed = [(e,d,c),(d,c,a),(c,a,b)] + flat = map(lambda row: itertools.chain(*row), boxed) + self.assertEqual(map(list, pixels), map(list, flat)) + def testRGBtoRGBA(self): + "asRGBA8() on colour type 2 source.""" + # Test for Issue 26 + r = Reader(bytes=_pngsuite['basn2c08']) + x,y,pixels,meta = r.asRGBA8() + # Test the pixels at row 9 columns 0 and 1. + row9 = list(pixels)[9] + self.assertEqual(row9[0:8], + [0xff, 0xdf, 0xff, 0xff, 0xff, 0xde, 0xff, 0xff]) + def testCtrns(self): + "Test colour type 2 and tRNS chunk." + # Test for Issue 25 + r = Reader(bytes=_pngsuite['tbrn2c08']) + x,y,pixels,meta = r.asRGBA8() + # I just happen to know that the first pixel is transparent. + # In particular it should be #7f7f7f00 + row0 = list(pixels)[0] + self.assertEqual(tuple(row0[0:4]), (0x7f, 0x7f, 0x7f, 0x00)) + def testAdam7read(self): + """Adam7 interlace reading. + Specifically, test that for images in the PngSuite that + have both an interlaced and straightlaced pair that both + images from the pair produce the same array of pixels.""" + for candidate in _pngsuite: + if not candidate.startswith('basn'): + continue + candi = candidate.replace('n', 'i') + if candi not in _pngsuite: + continue + print 'adam7 read', candidate + straight = Reader(bytes=_pngsuite[candidate]) + adam7 = Reader(bytes=_pngsuite[candi]) + # Just compare the pixels. Ignore x,y (because they're + # likely to be correct?); metadata is ignored because the + # "interlace" member differs. Lame. + straight = straight.read()[2] + adam7 = adam7.read()[2] + self.assertEqual(map(list, straight), map(list, adam7)) + def testAdam7write(self): + """Adam7 interlace writing. + For each test image in the PngSuite, write an interlaced + and a straightlaced version. Decode both, and compare results. + """ + # Not such a great test, because the only way we can check what + # we have written is to read it back again. + + for name,bytes in _pngsuite.items(): + # Only certain colour types supported for this test. + if name[3:5] not in ['n0', 'n2', 'n4', 'n6']: + continue + it = Reader(bytes=bytes) + x,y,pixels,meta = it.read() + pngi = topngbytes('adam7wn'+name+'.png', pixels, + x=x, y=y, bitdepth=it.bitdepth, + greyscale=it.greyscale, alpha=it.alpha, + transparent=it.transparent, + interlace=False) + x,y,ps,meta = Reader(bytes=pngi).read() + it = Reader(bytes=bytes) + x,y,pixels,meta = it.read() + pngs = topngbytes('adam7wi'+name+'.png', pixels, + x=x, y=y, bitdepth=it.bitdepth, + greyscale=it.greyscale, alpha=it.alpha, + transparent=it.transparent, + interlace=True) + x,y,pi,meta = Reader(bytes=pngs).read() + self.assertEqual(map(list, ps), map(list, pi)) + def testPGMin(self): + """Test that the command line tool can read PGM files.""" + def do(): + return _main(['testPGMin']) + s = StringIO() + s.write('P5 2 2 3\n') + s.write('\x00\x01\x02\x03') + s.flush() + s.seek(0) + o = StringIO() + testWithIO(s, o, do) + r = Reader(bytes=o.getvalue()) + x,y,pixels,meta = r.read() + self.assert_(r.greyscale) + self.assertEqual(r.bitdepth, 2) + def testPAMin(self): + """Test that the command line tool can read PAM file.""" + def do(): + return _main(['testPAMin']) + s = StringIO() + s.write('P7\nWIDTH 3\nHEIGHT 1\nDEPTH 4\nMAXVAL 255\n' + 'TUPLTYPE RGB_ALPHA\nENDHDR\n') + # The pixels in flat row flat pixel format + flat = [255,0,0,255, 0,255,0,120, 0,0,255,30] + s.write(''.join(map(chr, flat))) + s.flush() + s.seek(0) + o = StringIO() + testWithIO(s, o, do) + r = Reader(bytes=o.getvalue()) + x,y,pixels,meta = r.read() + self.assert_(r.alpha) + self.assert_(not r.greyscale) + self.assertEqual(list(itertools.chain(*pixels)), flat) + def testLA4(self): + """Create an LA image with bitdepth 4.""" + bytes = topngbytes('la4.png', [[5, 12]], 1, 1, + greyscale=True, alpha=True, bitdepth=4) + sbit = Reader(bytes=bytes).chunk('sBIT')[1] + self.assertEqual(sbit, '\x04\x04') + def testPNMsbit(self): + """Test that PNM files can generates sBIT chunk.""" + def do(): + return _main(['testPNMsbit']) + s = StringIO() + s.write('P6 8 1 1\n') + for pixel in range(8): + s.write(struct.pack('>sys.stderr, "skipping numpy test" + return + + rows = [map(numpy.uint16, range(0,0x10000,0x5555))] + b = topngbytes('numpyuint16.png', rows, 4, 1, + greyscale=True, alpha=False, bitdepth=16) + def testNumpyuint8(self): + """numpy uint8.""" + + try: + import numpy + except ImportError: + print >>sys.stderr, "skipping numpy test" + return + + rows = [map(numpy.uint8, range(0,0x100,0x55))] + b = topngbytes('numpyuint8.png', rows, 4, 1, + greyscale=True, alpha=False, bitdepth=8) + def testNumpybool(self): + """numpy bool.""" + + try: + import numpy + except ImportError: + print >>sys.stderr, "skipping numpy test" + return + + rows = [map(numpy.bool, [0,1])] + b = topngbytes('numpybool.png', rows, 2, 1, + greyscale=True, alpha=False, bitdepth=1) + def testNumpyarray(self): + """numpy array.""" + try: + import numpy + except ImportError: + print >>sys.stderr, "skipping numpy test" + return + + pixels = numpy.array([[0,0x5555],[0x5555,0xaaaa]], numpy.uint16) + img = from_array(pixels, 'L') + img.save('testnumpyL16.png') + +# === Command Line Support === + +def _dehex(s): + """Liberally convert from hex string to binary string.""" + import re + + # Remove all non-hexadecimal digits + s = re.sub(r'[^a-fA-F\d]', '', s) + return s.decode('hex') + +# Copies of PngSuite test files taken +# from http://www.schaik.com/pngsuite/pngsuite_bas_png.html +# on 2009-02-19 by drj and converted to hex. +# Some of these are not actually in PngSuite (but maybe they should +# be?), they use the same naming scheme, but start with a capital +# letter. +_pngsuite = { + 'basi0g01': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002001000000012c0677 +cf0000000467414d41000186a031e8965f0000009049444154789c2d8d310ec2 +300c45dfc682c415187a00a42e197ab81e83b127e00c5639001363a580d8582c +65c910357c4b78b0bfbfdf4f70168c19e7acb970a3f2d1ded9695ce5bf5963df +d92aaf4c9fd927ea449e6487df5b9c36e799b91bdf082b4d4bd4014fe4014b01 +ab7a17aee694d28d328a2d63837a70451e1648702d9a9ff4a11d2f7a51aa21e5 +a18c7ffd0094e3511d661822f20000000049454e44ae426082 +"""), + 'basi0g02': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002002000000016ba60d +1f0000000467414d41000186a031e8965f0000005149444154789c635062e860 +00e17286bb609c93c370ec189494960631366e4467b3ae675dcf10f521ea0303 +90c1ca006444e11643482064114a4852c710baea3f18c31918020c30410403a6 +0ac1a09239009c52804d85b6d97d0000000049454e44ae426082 +"""), + 'basi0g04': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200400000001e4e6f8 +bf0000000467414d41000186a031e8965f000000ae49444154789c658e5111c2 +301044171c141c141c041c843a287510ea20d441c041c141c141c04191102454 +03994998cecd7edcecedbb9bdbc3b2c2b6457545fbc4bac1be437347f7c66a77 +3c23d60db15e88f5c5627338a5416c2e691a9b475a89cd27eda12895ae8dfdab +43d61e590764f5c83a226b40d669bec307f93247701687723abf31ff83a2284b +a5b4ae6b63ac6520ad730ca4ed7b06d20e030369bd6720ed383290360406d24e +13811f2781eba9d34d07160000000049454e44ae426082 +"""), + 'basi0g08': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200800000001211615 +be0000000467414d41000186a031e8965f000000b549444154789cb5905d0ac2 +3010849dbac81c42c47bf843cf253e8878b0aa17110f214bdca6be240f5d21a5 +94ced3e49bcd322c1624115515154998aa424822a82a5624a1aa8a8b24c58f99 +999908130989a04a00d76c2c09e76cf21adcb209393a6553577da17140a2c59e +70ecbfa388dff1f03b82fb82bd07f05f7cb13f80bb07ad2fd60c011c3c588eef +f1f4e03bbec7ce832dca927aea005e431b625796345307b019c845e6bfc3bb98 +769d84f9efb02ea6c00f9bb9ff45e81f9f280000000049454e44ae426082 +"""), + 'basi0g16': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002010000000017186c9 +fd0000000467414d41000186a031e8965f000000e249444154789cb5913b0ec2 +301044c7490aa8f85d81c3e4301c8f53a4ca0da8902c8144b3920b4043111282 +23bc4956681a6bf5fc3c5a3ba0448912d91a4de2c38dd8e380231eede4c4f7a1 +4677700bec7bd9b1d344689315a3418d1a6efbe5b8305ba01f8ff4808c063e26 +c60d5c81edcf6c58c535e252839e93801b15c0a70d810ae0d306b205dc32b187 +272b64057e4720ff0502154034831520154034c3df81400510cdf0015c86e5cc +5c79c639fddba9dcb5456b51d7980eb52d8e7d7fa620a75120d6064641a05120 +b606771a05626b401a05f1f589827cf0fe44c1f0bae0055698ee8914fffffe00 +00000049454e44ae426082 +"""), + 'basi2c08': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002008020000018b1fdd +350000000467414d41000186a031e8965f000000f249444154789cd59341aa04 +210c44abc07b78133d59d37333bd89d76868b566d10cf4675af8596431a11662 +7c5688919280e312257dd6a0a4cf1a01008ee312a5f3c69c37e6fcc3f47e6776 +a07f8bdaf5b40feed2d33e025e2ff4fe2d4a63e1a16d91180b736d8bc45854c5 +6d951863f4a7e0b66dcf09a900f3ffa2948d4091e53ca86c048a64390f662b50 +4a999660ced906182b9a01a8be00a56404a6ede182b1223b4025e32c4de34304 +63457680c93aada6c99b73865aab2fc094920d901a203f5ddfe1970d28456783 +26cffbafeffcd30654f46d119be4793f827387fc0d189d5bc4d69a3c23d45a7f +db803146578337df4d0a3121fc3d330000000049454e44ae426082 +"""), + 'basi2c16': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000201002000001db8f01 +760000000467414d41000186a031e8965f0000020a49444154789cd5962173e3 +3010853fcf1838cc61a1818185a53e56787fa13fa130852e3b5878b4b0b03081 +b97f7030070b53e6b057a0a8912bbb9163b9f109ececbc59bd7dcf2b45492409 +d66f00eb1dd83cb5497d65456aeb8e1040913b3b2c04504c936dd5a9c7e2c6eb +b1b8f17a58e8d043da56f06f0f9f62e5217b6ba3a1b76f6c9e99e8696a2a72e2 +c4fb1e4d452e92ec9652b807486d12b6669be00db38d9114b0c1961e375461a5 +5f76682a85c367ad6f682ff53a9c2a353191764b78bb07d8ddc3c97c1950f391 +6745c7b9852c73c2f212605a466a502705c8338069c8b9e84efab941eb393a97 +d4c9fd63148314209f1c1d3434e847ead6380de291d6f26a25c1ebb5047f5f24 +d85c49f0f22cc1d34282c72709cab90477bf25b89d49f0f351822297e0ea9704 +f34c82bc94002448ede51866e5656aef5d7c6a385cb4d80e6a538ceba04e6df2 +480e9aa84ddedb413bb5c97b3838456df2d4fec2c7a706983e7474d085fae820 +a841776a83073838973ac0413fea2f1dc4a06e71108fda73109bdae48954ad60 +bf867aac3ce44c7c1589a711cf8a81df9b219679d96d1cec3d8bbbeaa2012626 +df8c7802eda201b2d2e0239b409868171fc104ba8b76f10b4da09f6817ffc609 +c413ede267fd1fbab46880c90f80eccf0013185eb48b47ba03df2bdaadef3181 +cb8976f18e13188768170f98c0f844bb78cb04c62ddac59d09fc3fa25dfc1da4 +14deb3df1344f70000000049454e44ae426082 +"""), + 'basi3p08': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020080300000133a3ba +500000000467414d41000186a031e8965f00000300504c5445224400f5ffed77 +ff77cbffff110a003a77002222ffff11ff110000222200ffac5566ff66ff6666 +ff01ff221200dcffffccff994444ff005555220000cbcbff44440055ff55cbcb +00331a00ffecdcedffffe4ffcbffdcdc44ff446666ff330000442200ededff66 +6600ffa444ffffaaeded0000cbcbfefffffdfffeffff0133ff33552a000101ff +8888ff00aaaa010100440000888800ffe4cbba5b0022ff22663200ffff99aaaa +ff550000aaaa00cb630011ff11d4ffaa773a00ff4444dc6b0066000001ff0188 +4200ecffdc6bdc00ffdcba00333300ed00ed7300ffff88994a0011ffff770000 +ff8301ffbabafe7b00fffeff00cb00ff999922ffff880000ffff77008888ffdc +ff1a33000000aa33ffff009900990000000001326600ffbaff44ffffffaaff00 +770000fefeaa00004a9900ffff66ff22220000998bff1155ffffff0101ff88ff +005500001111fffffefffdfea4ff4466ffffff66ff003300ffff55ff77770000 +88ff44ff00110077ffff006666ffffed000100fff5ed1111ffffff44ff22ffff +eded11110088ffff00007793ff2200dcdc3333fffe00febabaff99ffff333300 +63cb00baba00acff55ffffdcffff337bfe00ed00ed5555ffaaffffdcdcff5555 +00000066dcdc00dc00dc83ff017777fffefeffffffcbff5555777700fefe00cb +00cb0000fe010200010000122200ffff220044449bff33ffd4aa0000559999ff +999900ba00ba2a5500ffcbcbb4ff66ff9b33ffffbaaa00aa42880053aa00ffaa +aa0000ed00babaffff1100fe00000044009999990099ffcc99ba000088008800 +dc00ff93220000dcfefffeaa5300770077020100cb0000000033ffedff00ba00 +ff3333edffedffc488bcff7700aa00660066002222dc0000ffcbffdcffdcff8b +110000cb00010155005500880000002201ffffcbffcbed0000ff88884400445b +ba00ffbc77ff99ff006600baffba00777773ed00fe00003300330000baff77ff +004400aaffaafffefe000011220022c4ff8800eded99ff99ff55ff002200ffb4 +661100110a1100ff1111dcffbabaffff88ff88010001ff33ffb98ed362000002 +a249444154789c65d0695c0b001806f03711a9904a94d24dac63292949e5a810 +d244588a14ca5161d1a1323973252242d62157d12ae498c8124d25ca3a11398a +16e55a3cdffab0ffe7f77d7fcff3528645349b584c3187824d9d19d4ec2e3523 +9eb0ae975cf8de02f2486d502191841b42967a1ad49e5ddc4265f69a899e26b5 +e9e468181baae3a71a41b95669da8df2ea3594c1b31046d7b17bfb86592e4cbe +d89b23e8db0af6304d756e60a8f4ad378bdc2552ae5948df1d35b52143141533 +33bbbbababebeb3b3bc9c9c9c6c6c0c0d7b7b535323225a5aa8a02024a4bedec +0a0a2a2bcdcd7d7cf2f3a9a9c9cdcdd8b8adcdd5b5ababa828298982824a4ab2 +b21212acadbdbc1414e2e24859b9a72730302f4f49292c4c57373c9c0a0b7372 +8c8c1c1c3a3a92936d6dfdfd293e3e26262a4a4eaea2424b4b5fbfbc9c323278 +3c0b0ba1303abaae8ecdeeed950d6669a9a7a7a141d4de9e9d5d5cdcd2229b94 +c572716132f97cb1d8db9bc3110864a39795d9db6b6a26267a7a9a98d4d6a6a7 +cb76090ef6f030354d4d75766e686030545464cb393a1a1ac6c68686eae8f8f9 +a9aa4644c8b66d6e1689dcdd2512a994cb35330b0991ad9f9b6b659596a6addd +d8282fafae5e5323fb8f41d01f76c22fd8061be01bfc041a0323e1002c81cd30 +0b9ec027a0c930014ec035580fc3e112bc069a0b53e11c0c8095f00176c163a0 +e5301baec06a580677600ddc05ba0f13e120bc81a770133ec355a017300d4ec2 +0c7800bbe1219c02fa08f3e13c1c85dbb00a2ec05ea0dff00a6ec15a98027360 +070c047a06d7e1085c84f1b014f6c03fa0b33018b6c0211801ebe018fc00da0a +6f61113c877eb01d4ec317a085700f26c130f80efbe132bc039a0733e106fc81 +f7f017f6c10aa0d1300a0ec374780943e1382c06fa0a9b60238c83473016cec0 +02f80f73fefe1072afc1e50000000049454e44ae426082 +"""), + 'basi6a08': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200806000001047d4a +620000000467414d41000186a031e8965f0000012049444154789cc595414ec3 +3010459fa541b8bbb26641b8069b861e8b4d12c1c112c1452a710a2a65d840d5 +949041fc481ec98ae27c7f3f8d27e3e4648047600fec0d1f390fbbe2633a31e2 +9389e4e4ea7bfdbf3d9a6b800ab89f1bd6b553cfcbb0679e960563d72e0a9293 +b7337b9f988cc67f5f0e186d20e808042f1c97054e1309da40d02d7e27f92e03 +6cbfc64df0fc3117a6210a1b6ad1a00df21c1abcf2a01944c7101b0cb568a001 +909c9cf9e399cf3d8d9d4660a875405d9a60d000b05e2de55e25780b7a5268e0 +622118e2399aab063a815808462f1ab86890fc2e03e48bb109ded7d26ce4bf59 +0db91bac0050747fec5015ce80da0e5700281be533f0ce6d5900b59bcb00ea6d +200314cf801faab200ea752803a8d7a90c503a039f824a53f4694e7342000000 +0049454e44ae426082 +"""), + 'basn0g01': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002001000000005b0147 +590000000467414d41000186a031e8965f0000005b49444154789c2dccb10903 +300c05d1ebd204b24a200b7a346f90153c82c18d0a61450751f1e08a2faaead2 +a4846ccea9255306e753345712e211b221bf4b263d1b427325255e8bdab29e6f +6aca30692e9d29616ee96f3065f0bf1f1087492fd02f14c90000000049454e44 +ae426082 +"""), + 'basn0g02': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002002000000001ca13d +890000000467414d41000186a031e8965f0000001f49444154789c6360085df5 +1f8cf1308850c20053868f0133091f6390b90700bd497f818b0989a900000000 +49454e44ae426082 +"""), + # A version of basn0g04 dithered down to 3 bits. + 'Basn0g03': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020040000000093e1c8 +2900000001734249540371d88211000000fd49444154789c6d90d18906210c84 +c356f22356b2889588604301b112112b11d94a96bb495cf7fe87f32d996f2689 +44741cc658e39c0b118f883e1f63cc89dafbc04c0f619d7d898396c54b875517 +83f3a2e7ac09a2074430e7f497f00f1138a5444f82839c5206b1f51053cca968 +63258821e7f2b5438aac16fbecc052b646e709de45cf18996b29648508728612 +952ca606a73566d44612b876845e9a347084ea4868d2907ff06be4436c4b41a3 +a3e1774285614c5affb40dbd931a526619d9fa18e4c2be420858de1df0e69893 +a0e3e5523461be448561001042b7d4a15309ce2c57aef2ba89d1c13794a109d7 +b5880aa27744fc5c4aecb5e7bcef5fe528ec6293a930690000000049454e44ae +426082 +"""), + 'basn0g04': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020040000000093e1c8 +290000000467414d41000186a031e8965f0000004849444154789c6360601014 +545232367671090d4d4b2b2f6720430095dbd1418e002a77e64c720450b9ab56 +912380caddbd9b1c0154ee9933e408a072efde25470095fbee1d1902001f14ee +01eaff41fa0000000049454e44ae426082 +"""), + 'basn0g08': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200800000000561125 +280000000467414d41000186a031e8965f0000004149444154789c6364602400 +1408c8b30c05058c0f0829f8f71f3f6079301c1430ca11906764a2795c0c0605 +8c8ff0cafeffcff887e67131181430cae0956564040050e5fe7135e2d8590000 +000049454e44ae426082 +"""), + 'basn0g16': _dehex(""" +89504e470d0a1a0a0000000d49484452000000200000002010000000000681f9 +6b0000000467414d41000186a031e8965f0000005e49444154789cd5d2310ac0 +300c4351395bef7fc6dca093c0287b32d52a04a3d98f3f3880a7b857131363a0 +3a82601d089900dd82f640ca04e816dc06422640b7a03d903201ba05b7819009 +d02d680fa44c603f6f07ec4ff41938cf7f0016d84bd85fae2b9fd70000000049 +454e44ae426082 +"""), + 'basn2c08': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200802000000fc18ed +a30000000467414d41000186a031e8965f0000004849444154789cedd5c10900 +300c024085ec91fdb772133b442bf4a1f8cee12bb40d043b800a14f81ca0ede4 +7d4c784081020f4a871fc284071428f0a0743823a94081bb7077a3c00182b1f9 +5e0f40cf4b0000000049454e44ae426082 +"""), + 'basn2c16': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000201002000000ac8831 +e00000000467414d41000186a031e8965f000000e549444154789cd596c10a83 +301044a7e0417fcb7eb7fdadf6961e06039286266693cc7a188645e43dd6a08f +1042003e2fe09aef6472737e183d27335fcee2f35a77b702ebce742870a23397 +f3edf2705dd10160f3b2815fe8ecf2027974a6b0c03f74a6e4192843e75c6c03 +35e8ec3202f5e84c0181bbe8cca967a00d9df3491bb040671f2e6087ce1c2860 +8d1e05f8c7ee0f1d00b667e70df44467ef26d01fbd9bc028f42860f71d188bce +fb8d3630039dbd59601e7ab3c06cf428507f0634d039afdc80123a7bb1801e7a +b1802a7a14c89f016d74ce331bf080ce9e08f8414f04bca133bfe642fe5e07bb +c4ec0000000049454e44ae426082 +"""), + 'basn6a08': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200806000000737a7a +f40000000467414d41000186a031e8965f0000006f49444154789cedd6310a80 +300c46e12764684fa1f73f55048f21c4ddc545781d52e85028fc1f4d28d98a01 +305e7b7e9cffba33831d75054703ca06a8f90d58a0074e351e227d805c8254e3 +1bb0420f5cdc2e0079208892ffe2a00136a07b4007943c1004d900195036407f +011bf00052201a9c160fb84c0000000049454e44ae426082 +"""), + 'cs3n3p08': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020080300000044a48a +c60000000467414d41000186a031e8965f0000000373424954030303a392a042 +00000054504c544592ff0000ff9200ffff00ff0000dbff00ff6dffb600006dff +b6ff00ff9200dbff000049ffff2400ff000024ff0049ff0000ffdb00ff4900ff +b6ffff0000ff2400b6ffffdb000092ffff6d000024ffff49006dff00df702b17 +0000004b49444154789c85cac70182000000b1b3625754b0edbfa72324ef7486 +184ed0177a437b680bcdd0031c0ed00ea21f74852ed00a1c9ed0086da0057487 +6ed0121cd6d004bda0013a421ff803224033e177f4ae260000000049454e44ae +426082 +"""), + 's09n3p02': _dehex(""" +89504e470d0a1a0a0000000d49484452000000090000000902030000009dffee +830000000467414d41000186a031e8965f000000037342495404040477f8b5a3 +0000000c504c544500ff000077ffff00ffff7700ff5600640000001f49444154 +789c63600002fbff0c0c56ab19182ca381581a4283f82071200000696505c36a +437f230000000049454e44ae426082 +"""), + 'tbgn3p08': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020080300000044a48a +c60000000467414d41000186a031e8965f00000207504c54457f7f7fafafafab +abab110000222200737300999999510d00444400959500959595e6e600919191 +8d8d8d620d00898989666600b7b700911600000000730d007373736f6f6faaaa +006b6b6b676767c41a00cccc0000f30000ef00d51e0055555567670000dd0051 +515100d1004d4d4de61e0038380000b700160d0d00ab00560d00090900009500 +009100008d003333332f2f2f2f2b2f2b2b000077007c7c001a05002b27000073 +002b2b2b006f00bb1600272727780d002323230055004d4d00cc1e00004d00cc +1a000d00003c09006f6f00002f003811271111110d0d0d55554d090909001100 +4d0900050505000d00e2e200000900000500626200a6a6a6a2a2a29e9e9e8484 +00fb00fbd5d500801100800d00ea00ea555500a6a600e600e6f7f700e200e233 +0500888888d900d9848484c01a007777003c3c05c8c8008080804409007c7c7c +bb00bbaa00aaa600a61e09056262629e009e9a009af322005e5e5e05050000ee +005a5a5adddd00a616008d008d00e20016050027270088110078780000c40078 +00787300736f006f44444400aa00c81e004040406600663c3c3c090000550055 +1a1a00343434d91e000084004d004d007c004500453c3c00ea1e00222222113c +113300331e1e1efb22001a1a1a004400afaf00270027003c001616161e001e0d +160d2f2f00808000001e00d1d1001100110d000db7b7b7090009050005b3b3b3 +6d34c4230000000174524e530040e6d86600000001624b474402660b7c640000 +01f249444154789c6360c0048c8c58049100575f215ee92e6161ef109cd2a15e +4b9645ce5d2c8f433aa4c24f3cbd4c98833b2314ab74a186f094b9c2c27571d2 +6a2a58e4253c5cda8559057a392363854db4d9d0641973660b0b0bb76bb16656 +06970997256877a07a95c75a1804b2fbcd128c80b482a0b0300f8a824276a9a8 +ec6e61612b3e57ee06fbf0009619d5fac846ac5c60ed20e754921625a2daadc6 +1967e29e97d2239c8aec7e61fdeca9cecebef54eb36c848517164514af16169e +866444b2b0b7b55534c815cc2ec22d89cd1353800a8473100a4485852d924a6a +412adc74e7ad1016ceed043267238c901716f633a812022998a4072267c4af02 +92127005c0f811b62830054935ce017b38bf0948cc5c09955f030a24617d9d46 +63371fd940b0827931cbfdf4956076ac018b592f72d45594a9b1f307f3261b1a +084bc2ad50018b1900719ba6ba4ca325d0427d3f6161449486f981144cf3100e +2a5f2a1ce8683e4ddf1b64275240c8438d98af0c729bbe07982b8a1c94201dc2 +b3174c9820bcc06201585ad81b25b64a2146384e3798290c05ad280a18c0a62e +e898260c07fca80a24c076cc864b777131a00190cdfa3069035eccbc038c30e1 +3e88b46d16b6acc5380d6ac202511c392f4b789aa7b0b08718765990111606c2 +9e854c38e5191878fbe471e749b0112bb18902008dc473b2b2e8e72700000000 +49454e44ae426082 +"""), + 'Tp2n3p08': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020080300000044a48a +c60000000467414d41000186a031e8965f00000300504c544502ffff80ff05ff +7f0703ff7f0180ff04ff00ffff06ff000880ff05ff7f07ffff06ff000804ff00 +0180ff02ffff03ff7f02ffff80ff0503ff7f0180ffff0008ff7f0704ff00ffff +06ff000802ffffff7f0704ff0003ff7fffff0680ff050180ff04ff000180ffff +0008ffff0603ff7f80ff05ff7f0702ffffff000880ff05ffff0603ff7f02ffff +ff7f070180ff04ff00ffff06ff000880ff050180ffff7f0702ffff04ff0003ff +7fff7f0704ff0003ff7f0180ffffff06ff000880ff0502ffffffff0603ff7fff +7f0702ffff04ff000180ff80ff05ff0008ff7f07ffff0680ff0504ff00ff0008 +0180ff03ff7f02ffff02ffffffff0604ff0003ff7f0180ffff000880ff05ff7f +0780ff05ff00080180ff02ffffff7f0703ff7fffff0604ff00ff7f07ff0008ff +ff0680ff0504ff0002ffff0180ff03ff7fff0008ffff0680ff0504ff000180ff +02ffff03ff7fff7f070180ff02ffff04ff00ffff06ff0008ff7f0780ff0503ff +7fffff06ff0008ff7f0780ff0502ffff03ff7f0180ff04ff0002ffffff7f07ff +ff0604ff0003ff7fff00080180ff80ff05ffff0603ff7f0180ffff000804ff00 +80ff0502ffffff7f0780ff05ffff0604ff000180ffff000802ffffff7f0703ff +7fff0008ff7f070180ff03ff7f02ffff80ff05ffff0604ff00ff0008ffff0602 +ffff0180ff04ff0003ff7f80ff05ff7f070180ff04ff00ff7f0780ff0502ffff +ff000803ff7fffff0602ffffff7f07ffff0680ff05ff000804ff0003ff7f0180 +ff02ffff0180ffff7f0703ff7fff000804ff0080ff05ffff0602ffff04ff00ff +ff0603ff7fff7f070180ff80ff05ff000803ff7f0180ffff7f0702ffffff0008 +04ff00ffff0680ff0503ff7f0180ff04ff0080ff05ffff06ff000802ffffff7f +0780ff05ff0008ff7f070180ff03ff7f04ff0002ffffffff0604ff00ff7f07ff +000880ff05ffff060180ff02ffff03ff7f80ff05ffff0602ffff0180ff03ff7f +04ff00ff7f07ff00080180ffff000880ff0502ffff04ff00ff7f0703ff7fffff +06ff0008ffff0604ff00ff7f0780ff0502ffff03ff7f0180ffdeb83387000000 +f874524e53000000000000000008080808080808081010101010101010181818 +1818181818202020202020202029292929292929293131313131313131393939 +393939393941414141414141414a4a4a4a4a4a4a4a52525252525252525a5a5a +5a5a5a5a5a62626262626262626a6a6a6a6a6a6a6a73737373737373737b7b7b +7b7b7b7b7b83838383838383838b8b8b8b8b8b8b8b94949494949494949c9c9c +9c9c9c9c9ca4a4a4a4a4a4a4a4acacacacacacacacb4b4b4b4b4b4b4b4bdbdbd +bdbdbdbdbdc5c5c5c5c5c5c5c5cdcdcdcdcdcdcdcdd5d5d5d5d5d5d5d5dedede +dededededee6e6e6e6e6e6e6e6eeeeeeeeeeeeeeeef6f6f6f6f6f6f6f6b98ac5 +ca0000012c49444154789c6360e7169150d230b475f7098d4ccc28a96ced9e32 +63c1da2d7b8e9fb97af3d1fb8f3f18e8a0808953544a4dd7c4c2c9233c2621bf +b4aab17fdacce5ab36ee3a72eafaad87efbefea68702362e7159652d031b07cf +c0b8a4cce28aa68e89f316aedfb4ffd0b92bf79fbcfcfe931e0a183904e55435 +8decdcbcc22292b3caaadb7b27cc5db67af3be63e72fdf78fce2d31f7a2860e5 +119356d037b374f10e8a4fc92eaa6fee99347fc9caad7b0f9ebd74f7c1db2fbf +e8a180995f484645dbdccad12f38363dafbcb6a573faeca5ebb6ed3e7ce2c29d +e76fbefda38702063e0149751d537b67ff80e8d4dcc29a86bea97316add9b0e3 +c0e96bf79ebdfafc971e0a587885e515f58cad5d7d43a2d2720aeadaba26cf5a +bc62fbcea3272fde7efafac37f3a28000087c0fe101bc2f85f0000000049454e +44ae426082 +"""), + 'tbbn1g04': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020040000000093e1c8 +290000000467414d41000186a031e8965f0000000274524e530007e8f7589b00 +000002624b47440000aa8d23320000013e49444154789c55d1cd4b024118c7f1 +efbe6419045b6a48a72d352808b435284f9187ae9b098627a1573a19945beba5 +e8129e8222af11d81e3a4545742de8ef6af6d5762e0fbf0fc33c33f36085cb76 +bc4204778771b867260683ee57e13f0c922df5c719c2b3b6c6c25b2382cea4b9 +9f7d4f244370746ac71f4ca88e0f173a6496749af47de8e44ba8f3bf9bdfa98a +0faf857a7dd95c7dc8d7c67c782c99727997f41eb2e3c1e554152465bb00fe8e +b692d190b718d159f4c0a45c4435915a243c58a7a4312a7a57913f05747594c6 +46169866c57101e4d4ce4d511423119c419183a3530cc63db88559ae28e7342a +1e9c8122b71139b8872d6e913153224bc1f35b60e4445bd4004e20ed6682c759 +1d9873b3da0fbf50137dc5c9bde84fdb2ec8bde1189e0448b63584735993c209 +7a601bd2710caceba6158797285b7f2084a2f82c57c01a0000000049454e44ae +426082 +"""), + 'tbrn2c08': _dehex(""" +89504e470d0a1a0a0000000d4948445200000020000000200802000000fc18ed +a30000000467414d41000186a031e8965f0000000674524e53007f007f007f8a +33334f00000006624b474400ff0000000033277cf3000004d649444154789cad +965f68537714c73fd912d640235e692f34d0406fa0c1663481045ab060065514 +56660a295831607df0a1488715167060840a1614e6431e9cb34fd2c00a762c85 +f6a10f816650c13b0cf40612e1822ddc4863bd628a8924d23d6464f9d3665dd9 +f7e977ce3dbff3cd3939bfdfef6bb87dfb364782dbed065ebe7cd93acc78b4ec +a228debd7bb7bfbfbfbbbbfb7f261045311a8d261209405194274f9ea4d3e916 +f15f1c3eb5dd6e4fa5fecce526239184a2b0b8486f6f617171b1f5ae4311381c +8e57af5e5dbd7a351088150a78bd389d44222c2f93cdfe66b7db8f4ee07038b6 +b6b6bebf766d7e7e7e60a06432313b4ba984c3c1c4049a46b95c5a58583822c1 +dbb76f27272733d1b9df853c3030c0f232562b9108cf9eb1b888d7cbf030abab +31abd5fa1f08dc6ef7e7cf9f1f3f7e1c8944745d4f1400c62c001313acad21cb +b8dd2c2c603271eb1640341aad4c6d331aa7e8c48913a150a861307ecc11e964 +74899919bc5e14e56fffc404f1388502f178dceff7ef4bf0a5cfe7abb533998c +e5f9ea2f1dd88c180d64cb94412df3dd57e83a6b3b3c7a84c98420100c72fd3a +636348bae726379fe69e8e8d8dbd79f3a6558b0607079796965256479b918085 +7b02db12712b6181950233023f3f647494ee6e2e5ea45864cce5b8a7fe3acffc +3aebb22c2bd5d20e22d0757d7b7bbbbdbd3d94a313bed1b0aa3cd069838b163a +8d4c59585f677292d0b84d9a995bd337def3fe6bbe5e6001989b9b6bfe27ea08 +36373781542ab56573248b4c5bc843ac4048c7ab21aa24ca00534c25482828a3 +8c9ee67475bbaaaab22cb722c8e57240a150301a8d219de94e44534d7d90e885 +87acb0e2c4f9800731629b6c5ee14a35a6b9887d2a0032994cb9cf15dbe59650 +ff7b46a04c9a749e7cc5112214266cc65c31354d5b5d5d3d90209bcd5616a552 +a95c2e87f2a659bd9ee01c2cd73964e438f129a6aa9e582c363838b80f81d7eb +5555b56a2a8ad2d9d7affd0409f8015c208013fea00177b873831b0282c964f2 +783c1e8fa7582cee5f81a669b5e6eeeeaee58e8559b0c233d8843c7c0b963a82 +34e94b5cb2396d7d7d7db22c8ba258fb0afd43f0e2c58b919191ba9de9b4d425 +118329b0c3323c8709d02041b52b4ea7f39de75d2a934a2693c0a953a76a93d4 +5d157ebf7f6565a5542a553df97c5e10045dd731c130b86113cc300cbd489224 +08422a952a140a95788fc763b1d41558d7a2d7af5f5fb870a1d6a3aaaacd6603 +18802da84c59015bd2e6897b745d9765b99a1df0f97c0daf74e36deaf7fbcd66 +73ad2797cb89a2c839880188a2e8743a8bc5a22ccbba5e376466b3b9bdbdbd21 +6123413a9d0e0402b51e4dd3bababa788eb022b85caeb6b6364551b6b7b76942 +43f7f727007a7a7a04a1ee8065b3595fde2768423299ac1ec6669c3973e65004 +c0f8f878ad69341a33994ced2969c0d0d0502412f9f8f163f3a7fd654b474787 +288ad53e74757535df6215b85cae60302849d2410aecc037f9f2e5cbd5b5c160 +680eb0dbede170381c0e7ff8f0a185be3b906068684892a4ca7a6f6faff69328 +8ad3d3d3f7efdfdfdbdbfb57e96868a14d0d0643381c96242997cbe5f3794010 +84603078fcf8f1d6496bd14a3aba5c2ea7d369341a5555b5582c8140e0fcf9f3 +1b1b1b87cf4eeb0a8063c78e45a3d19e9e1ebfdfdf5a831e844655d18093274f +9e3d7bf6d3a74f3b3b3b47c80efc05ff7af28fefb70d9b0000000049454e44ae +426082 +"""), + 'basn6a16': _dehex(""" +89504e470d0a1a0a0000000d494844520000002000000020100600000023eaa6 +b70000000467414d41000186a031e8965f00000d2249444154789cdd995f6c1c +d775c67ff38fb34b724d2ee55a8e4b04a0ac87049100cab4dbd8c6528902cb4d +10881620592e52d4325ac0905bc98a94025e71fd622cb5065ac98a0c283050c0 +728a00b6e542a1d126885cd3298928891d9a0444037e904434951d4b90b84b2f +c9dde1fcebc33977a95555348f411e16dfce9d3b77ee77eebde77ce78c95a669 +0ad07c17009a13edd898b87dfb1fcb7d2b4d1bff217f33df80deb1e6267df0ff +c1e6e6dfafdf1f5a7fd30f9aef66b6d546dd355bf02c40662e3307f9725a96c6 +744c3031f83782f171c148dbc3bf1774f5dad1e79d6f095a3f54d4fbec5234ef +d9a2f8d73afe4f14f57ef4f42def7b44f19060f06b45bddf1c5534d77fd922be +2973a15a82e648661c6e3240aa3612ead952b604bde57458894f29deaf133bac +13d2766f5227a4a3b8cf08da7adfd6fbd6bd8a4fe9dbb43d35e3dfa3f844fbf8 +9119bf4f7144094fb56333abf8a86063ca106f94b3a3b512343765e60082097f +1bb86ba72439a653519b09f5cee1ce61c897d37eedf5553580ae60f4af8af33a +b14fd400b6a0f34535c0434afc0b3a9f07147527a5fa7ca218ff56c74d74dc3f +155cfd3325fc278acf2ae1cb4a539f5f9937c457263b0bd51234c732a300cdd1 +cc1840f0aaff54db0e4874ed5a9b5d6d27d4bb36746d80de72baa877ff4b275a +d7895ed1897ea4139b5143fcbb1a62560da1ed9662aaed895ec78a91c18795b8 +5e07ab4af8ba128e95e682e0728bf8f2e5ae815a091a53d902ac1920d8e05f06 +589de8d8d66680789f4e454fb9d9ec66cd857af796ee2d902fa73fd5bba775a2 +153580ae44705ed0d37647d15697cb8f14bfa3e3e8fdf8031d47af571503357c +f30d25acedcbbf135c9a35c49766ba07ab255859e8ec03684e66860182dff8f7 +0304bff6ff1c20fc81b7afdd00a71475539a536e36bb5973a19e3b923b02bde5 +e4efd4003ac170eb2d13fe274157afedbd82d6fb3a9a1e85e4551d47cf7078f8 +9671fe4289ebf5f2bf08d63f37c4eb4773c55a0996efeefa0ca011671d8060ca +2f0004c7fcc300e166ef0240f825efe3361f106d57d423d0723f7acacd66376b +2ed47b7a7a7a205f4ef4ac4691e0aad9aa0d41cf13741c3580a506487574ddca +61a8c403c1863ebfbcac3475168b2de28b8b3d77544bb05ce92a02aceced3c0d +d0cc65ea371b201cf1c601c24dde1c4078cedbdeb60322f50126a019bf6edc9b +39e566b39b3517eaf97c3e0fbde5e4491d45bd74537145d155b476aa0176e868 +c6abebf30dbd5e525c54ac8e18e2d56abeb756827a3d970358a97416019a6f64 +f60004fdfe1580d5c98e618070cc1b05887eee7e0d209a70db7d8063029889b4 +c620ead78d7b33a7dc6c76b3e6427ddddbebde867c393aa7845e5403e8ca794a +d0d6fb897af5f03525fe5782f5e7046bdaef468bf88d1debc6ab25583cd17310 +6079b9ab0ba059c914018245bf076075b5a303200c3c1f209a733701444fbbaf +00c4134ebb016c5d0b23614c243701cdf875e3decce9349bddacb9505fbf7dfd +76e82d87736a00f5d2b5ffd4b7dce2719a4d25ae717ee153c1abef18e257cfad +7fa45682da48ef38c052b53b0fd06864b300c151ff08c0ea431de701a287dd5f +004497dc7b01a253ee3e80b8c7f91c20f967fb6fdb7c80ada7d8683723614c24 +3701cdf875e3decc29379bddacb950ef3fd47f08f2e5a61ea4aa2a3eb757cd55 +13345efcfa59c12b2f19e2578ef77fb75a82854ffbee01a83f977b11a031931d +040802df07082b5e11207cc17b1e209a770700e2df0a83e409fb7580f827c230 +99b06fd901fb058d6835dacd481813c94d40337eddb83773cacd66376b2ed437 +bebcf165e82d2f4e4beb7f3fa6e652c2d7ee10bc78c010bfb87fe3c95a09ae9f +bd732740bd2fb700d0f865f64180e059ff044018ca0ca28a5b04883f701e0088 +bfec7c0c909cb71f0448c6ec518074b375012079d9dedf66004bcfbc51eb2dd1 +aadacd481813c94d40337eddb83773cacd66376b2ed487868686205fbe7c49ef +5605a73f34c4a7a787eeab96e0da81bb4e022c15ba27019a5b339300e16bf286 +a8eae601e25866907cdf3e0890acb36f00245fb57f05904e59c300e92561946e +b2e600d209ab7d07f04d458dfb46ad1bd16ab49b913026929b8066fcba716fe6 +949bcd6ed65ca8ef7e7cf7e3d05b7e7c8f217ee6cdddbb6a25a856f37980e0c7 +fe4e80a82623c48193014846ec7180f4acf518409aca0cd28a5504e03b32c374 +de1a00608a0240faaa327a4b19fe946fb6f90054dbb5f2333d022db56eb4966a +3723614c243701cdf8f556bea8a7dc6c76b3e66bd46584ddbbcebc0990cf4b0f +ff4070520c282338a7e26700ec725202b01e4bcf0258963c6f1d4d8f0030cb20 +805549c520930c03584fa522b676f11600ffc03fde3e1b3489a9c9054c9aa23b +c08856a3dd8c843191dc0434e3d78d7b33a75c36fb993761f7ae5a69f72ef97f +e6ad336fed7e1c60e8bee96980bbdebbb60da07b7069062033d9dc0ae03d296f +70ab511ec071640676252902d833c916007b3e1900b0a6d2028035968e025861 +ea01581369fb11488c34d18cbc95989afccca42baad65ba2d5683723614c24d7 +8066fcbab8b7e96918baaf5aaa56219f975fb50a43f7c9bde90fa73f1c1a02d8 +78f2e27e803b77ca08b90519315b6fe400fc1392097a9eccc0ad444500e70199 +a1331f0f00d8934901c07e5d526ceb87c2d07e2579badd005a2b31a5089391b7 +1253358049535a6add8856dd0146c298482e01ede27ed878b256ba7600ee3a09 +c18fc1df09fe01084ec25defc1b56db0f1a4f4bd78e0e2818d2f0334e7330300 +7df7c888b917e50dd9c1c60c80efcb0cbc63e1f700bce7c31700dccbd1060027 +8add9b0de06c8e2f00d84962b7d7030e2a61538331b98051f92631bd253f336a +dd8856a3dd44c25c390efddfad96ae9f853b77c25201ba27c533b8bdf28b6ad0 +3d084b33d2e7fa59099e9901b8f2d29597fa0f01848f78e70082117f1ca07b76 +6910209b9519f895a008d031bbba05c09d8f06005c5b18b8fba25300cea6780e +c03e911c6ccf06d507b48a4fa606634a114609de929f9934c5a87511ad57cfc1 +fa476aa5854fa1ef1e3910b905686e85cc24c40138198915f133d2d6dc2a7dea +7df2ccc2a752faf2cec1d577aebeb37e3b4034eeee0008dff3be0e6b923773b4 +7904c0ef9119767cb4fa1500ef1361e08e452500f71561e84cc4ed3e20fab6a2 +c905f40cb76a3026bf3319b91ac2e46792a6dcd801ebc6aba5da08f48ecb81c8 +bd088d5f42f6417191de93908c803d0e76199292b485af41b60e8d9c3c537f0e +8211f0c7211a077707dc18b931b2ee6d80a4d7ae024491ebc24d4a708ff70680 +7f25e807e8785f1878e322d6ddaf453f0770ff2dfa769b01423dbbad72a391b6 +5a7c3235985629423372494cab55c8f7d64a8b27a0e7202c55a13b0f8d19c80e +4ae9ca3f015115dc3ca467c17a4c7ee95970ab10e5a54ff0ac3cd39881ee5958 +1a84f03df0be0e492fd855a8d6aa35d10b4962dbb0a604a3d3ee5e80a8eee600 +a24977f8660378bf0bbf00e01d0a8fb7f980f04b8aa6ce6aca8d5a7533c52753 +839152c4e222f4dc512dd5eb90cbc981e8ea12cf90cd8a8bf47d89159e2741d3 +7124f65b96fcd254dae258fa84a13c13043246a32129574787e49eae2b49b86d +c3e2e78b9ff7f4002415bb08907c66df0d103b4e0c104db90500ff70700c203a +ee1e82dba4c3e16e256c0acca6ceaae9afd1f612d7eb472157ac95962bd05594 +7dd1598466053245088e827f44628657942a825b84e4fb601f84b4025611aca3 +901e01bb024911dc0a4445f08e41f83df02b10142173149ab71baf027611ea95 +7a257704201d14cd9af4d90b00f194530088cb4e09c0df1c5c0088f7393f6833 +c0aa3ac156655de3bca9b34ab9716906ba07aba5e5bba1eb3358d90b9da7c533 +64f6888bf47b60f521e8380fe10be03d2feac17900927560df40f4e48f805960 +50328d648bf4893f9067c217a0631656b7c898c122847bc07b03a2d3e0ee85e4 +33b0ef867450c4fad2ecd26cf7168074c0ba0c904cdac300c9cfec4701924df6 +1cdca61e10685c6f7d52d0caba1498972f43d740adb4b2009d7d7220b20e3473 +90a943d00ffe959bb6eac3e0fe42ea49ee00c45f06e76329b1dabf127d690d80 +5581b408f63c2403e0cc433c00ee658836803b0fd100747c04ab5f917704fd10 +d5c1cd41ec801343d207f602a403605d86e5f9e5f9ae0d00e994556833806685 +c931fb709b0f08b4e869bea5c827859549e82c544b8d29c816a0390999613920 +7e610d5727a16318c2003c1fa24be0de2b32caf92224e7c17e5004b6350c4c01 +05601218066b0ad28224e149019c086257ca315102de2712903bde97b8144d82 +3b2c6ac52d403c054e019249b087f53d0558995a99ea946c70cc927458b3c1ff +550f30050df988d4284376b4566a8e416654cc921985e037e0df0fc131f00f4b +acf0c6211c036f14a239703741740adc7da227edd7e56b833d0ae92549b4d357 +25dfb49ed2ff63908e6adf27d6d0dda7638d4154d2778daca17f58e61297c129 +41f233b01f5dc3740cac51688c35c6b22580f48224fee9b83502569a66b629f1 +09f3713473413e2666e7fe6f6c6efefdfafda1f56f6e06f93496d9d67cb7366a +9964b6f92e64b689196ec6c604646fd3fe4771ff1bf03f65d8ecc3addbb5f300 +00000049454e44ae426082 +"""), +} + +def test_suite(options, args): + """ + Create a PNG test image and write the file to stdout. + """ + + # Below is a big stack of test image generators. + # They're all really tiny, so PEP 8 rules are suspended. + + def test_gradient_horizontal_lr(x, y): return x + def test_gradient_horizontal_rl(x, y): return 1-x + def test_gradient_vertical_tb(x, y): return y + def test_gradient_vertical_bt(x, y): return 1-y + def test_radial_tl(x, y): return max(1-math.sqrt(x*x+y*y), 0.0) + def test_radial_center(x, y): return test_radial_tl(x-0.5, y-0.5) + def test_radial_tr(x, y): return test_radial_tl(1-x, y) + def test_radial_bl(x, y): return test_radial_tl(x, 1-y) + def test_radial_br(x, y): return test_radial_tl(1-x, 1-y) + def test_stripe(x, n): return float(int(x*n) & 1) + def test_stripe_h_2(x, y): return test_stripe(x, 2) + def test_stripe_h_4(x, y): return test_stripe(x, 4) + def test_stripe_h_10(x, y): return test_stripe(x, 10) + def test_stripe_v_2(x, y): return test_stripe(y, 2) + def test_stripe_v_4(x, y): return test_stripe(y, 4) + def test_stripe_v_10(x, y): return test_stripe(y, 10) + def test_stripe_lr_10(x, y): return test_stripe(x+y, 10) + def test_stripe_rl_10(x, y): return test_stripe(1+x-y, 10) + def test_checker(x, y, n): return float((int(x*n) & 1) ^ (int(y*n) & 1)) + def test_checker_8(x, y): return test_checker(x, y, 8) + def test_checker_15(x, y): return test_checker(x, y, 15) + def test_zero(x, y): return 0 + def test_one(x, y): return 1 + + test_patterns = { + 'GLR': test_gradient_horizontal_lr, + 'GRL': test_gradient_horizontal_rl, + 'GTB': test_gradient_vertical_tb, + 'GBT': test_gradient_vertical_bt, + 'RTL': test_radial_tl, + 'RTR': test_radial_tr, + 'RBL': test_radial_bl, + 'RBR': test_radial_br, + 'RCTR': test_radial_center, + 'HS2': test_stripe_h_2, + 'HS4': test_stripe_h_4, + 'HS10': test_stripe_h_10, + 'VS2': test_stripe_v_2, + 'VS4': test_stripe_v_4, + 'VS10': test_stripe_v_10, + 'LRS': test_stripe_lr_10, + 'RLS': test_stripe_rl_10, + 'CK8': test_checker_8, + 'CK15': test_checker_15, + 'ZERO': test_zero, + 'ONE': test_one, + } + + def test_pattern(width, height, bitdepth, pattern): + """Create a single plane (monochrome) test pattern. Returns a + flat row flat pixel array. + """ + + maxval = 2**bitdepth-1 + if maxval > 255: + a = array('H') + else: + a = array('B') + fw = float(width) + fh = float(height) + pfun = test_patterns[pattern] + for y in range(height): + fy = float(y)/fh + for x in range(width): + a.append(int(round(pfun(float(x)/fw, fy) * maxval))) + return a + + def test_rgba(size=256, bitdepth=8, + red="GTB", green="GLR", blue="RTL", alpha=None): + """ + Create a test image. Each channel is generated from the + specified pattern; any channel apart from red can be set to + None, which will cause it not to be in the image. It + is possible to create all PNG channel types (L, RGB, LA, RGBA), + as well as non PNG channel types (RGA, and so on). + """ + + i = test_pattern(size, size, bitdepth, red) + psize = 1 + for channel in (green, blue, alpha): + if channel: + c = test_pattern(size, size, bitdepth, channel) + i = interleave_planes(i, c, psize, 1) + psize += 1 + return i + + def pngsuite_image(name): + """ + Create a test image by reading an internal copy of the files + from the PngSuite. Returned in flat row flat pixel format. + """ + + if name not in _pngsuite: + raise NotImplementedError("cannot find PngSuite file %s (use -L for a list)" % name) + r = Reader(bytes=_pngsuite[name]) + w,h,pixels,meta = r.asDirect() + assert w == h + # LAn for n < 8 is a special case for which we need to rescale + # the data. + if meta['greyscale'] and meta['alpha'] and meta['bitdepth'] < 8: + factor = 255 // (2**meta['bitdepth']-1) + def rescale(data): + for row in data: + yield map(factor.__mul__, row) + pixels = rescale(pixels) + meta['bitdepth'] = 8 + arraycode = 'BH'[meta['bitdepth']>8] + return w, array(arraycode, itertools.chain(*pixels)), meta + + # The body of test_suite() + size = 256 + if options.test_size: + size = options.test_size + options.bitdepth = options.test_depth + options.greyscale=bool(options.test_black) + + kwargs = {} + if options.test_red: + kwargs["red"] = options.test_red + if options.test_green: + kwargs["green"] = options.test_green + if options.test_blue: + kwargs["blue"] = options.test_blue + if options.test_alpha: + kwargs["alpha"] = options.test_alpha + if options.greyscale: + if options.test_red or options.test_green or options.test_blue: + raise ValueError("cannot specify colours (R, G, B) when greyscale image (black channel, K) is specified") + kwargs["red"] = options.test_black + kwargs["green"] = None + kwargs["blue"] = None + options.alpha = bool(options.test_alpha) + if not args: + pixels = test_rgba(size, options.bitdepth, **kwargs) + else: + size,pixels,meta = pngsuite_image(args[0]) + for k in ['bitdepth', 'alpha', 'greyscale']: + setattr(options, k, meta[k]) + + writer = Writer(size, size, + bitdepth=options.bitdepth, + transparent=options.transparent, + background=options.background, + gamma=options.gamma, + greyscale=options.greyscale, + alpha=options.alpha, + compression=options.compression, + interlace=options.interlace) + writer.write_array(sys.stdout, pixels) + +def read_pam_header(infile): + """ + Read (the rest of a) PAM header. `infile` should be positioned + immediately after the initial 'P7' line (at the beginning of the + second line). Returns are as for `read_pnm_header`. + """ + + # Unlike PBM, PGM, and PPM, we can read the header a line at a time. + header = dict() + while True: + l = infile.readline().strip() + if l == 'ENDHDR': + break + if l == '': + raise EOFError('PAM ended prematurely') + if l[0] == '#': + continue + l = l.split(None, 1) + if l[0] not in header: + header[l[0]] = l[1] + else: + header[l[0]] += ' ' + l[1] + + if ('WIDTH' not in header or + 'HEIGHT' not in header or + 'DEPTH' not in header or + 'MAXVAL' not in header): + raise Error('PAM file must specify WIDTH, HEIGHT, DEPTH, and MAXVAL') + width = int(header['WIDTH']) + height = int(header['HEIGHT']) + depth = int(header['DEPTH']) + maxval = int(header['MAXVAL']) + if (width <= 0 or + height <= 0 or + depth <= 0 or + maxval <= 0): + raise Error( + 'WIDTH, HEIGHT, DEPTH, MAXVAL must all be positive integers') + return 'P7', width, height, depth, maxval + +def read_pnm_header(infile, supported=('P5','P6')): + """ + Read a PNM header, returning (format,width,height,depth,maxval). + `width` and `height` are in pixels. `depth` is the number of + channels in the image; for PBM and PGM it is synthesized as 1, for + PPM as 3; for PAM images it is read from the header. `maxval` is + synthesized (as 1) for PBM images. + """ + + # Generally, see http://netpbm.sourceforge.net/doc/ppm.html + # and http://netpbm.sourceforge.net/doc/pam.html + + # Technically 'P7' must be followed by a newline, so by using + # rstrip() we are being liberal in what we accept. I think this + # is acceptable. + type = infile.read(3).rstrip() + if type not in supported: + raise NotImplementedError('file format %s not supported' % type) + if type == 'P7': + # PAM header parsing is completely different. + return read_pam_header(infile) + # Expected number of tokens in header (3 for P4, 4 for P6) + expected = 4 + pbm = ('P1', 'P4') + if type in pbm: + expected = 3 + header = [type] + + # We have to read the rest of the header byte by byte because the + # final whitespace character (immediately following the MAXVAL in + # the case of P6) may not be a newline. Of course all PNM files in + # the wild use a newline at this point, so it's tempting to use + # readline; but it would be wrong. + def getc(): + c = infile.read(1) + if c == '': + raise Error('premature EOF reading PNM header') + return c + + c = getc() + while True: + # Skip whitespace that precedes a token. + while c.isspace(): + c = getc() + # Skip comments. + while c == '#': + while c not in '\n\r': + c = getc() + if not c.isdigit(): + raise Error('unexpected character %s found in header' % c) + # According to the specification it is legal to have comments + # that appear in the middle of a token. + # This is bonkers; I've never seen it; and it's a bit awkward to + # code good lexers in Python (no goto). So we break on such + # cases. + token = '' + while c.isdigit(): + token += c + c = getc() + # Slight hack. All "tokens" are decimal integers, so convert + # them here. + header.append(int(token)) + if len(header) == expected: + break + # Skip comments (again) + while c == '#': + while c not in '\n\r': + c = getc() + if not c.isspace(): + raise Error('expected header to end with whitespace, not %s' % c) + + if type in pbm: + # synthesize a MAXVAL + header.append(1) + depth = (1,3)[type == 'P6'] + return header[0], header[1], header[2], depth, header[3] + +def write_pnm(file, width, height, pixels, meta): + """Write a Netpbm PNM/PAM file.""" + + bitdepth = meta['bitdepth'] + maxval = 2**bitdepth - 1 + # Rudely, the number of image planes can be used to determine + # whether we are L (PGM), LA (PAM), RGB (PPM), or RGBA (PAM). + planes = meta['planes'] + # Can be an assert as long as we assume that pixels and meta came + # from a PNG file. + assert planes in (1,2,3,4) + if planes in (1,3): + if 1 == planes: + # PGM + # Could generate PBM if maxval is 1, but we don't (for one + # thing, we'd have to convert the data, not just blat it + # out). + fmt = 'P5' + else: + # PPM + fmt = 'P6' + file.write('%s %d %d %d\n' % (fmt, width, height, maxval)) + if planes in (2,4): + # PAM + # See http://netpbm.sourceforge.net/doc/pam.html + if 2 == planes: + tupltype = 'GRAYSCALE_ALPHA' + else: + tupltype = 'RGB_ALPHA' + file.write('P7\nWIDTH %d\nHEIGHT %d\nDEPTH %d\nMAXVAL %d\n' + 'TUPLTYPE %s\nENDHDR\n' % + (width, height, planes, maxval, tupltype)) + # Values per row + vpr = planes * width + # struct format + fmt = '>%d' % vpr + if maxval > 0xff: + fmt = fmt + 'H' + else: + fmt = fmt + 'B' + for row in pixels: + file.write(struct.pack(fmt, *row)) + file.flush() + +def color_triple(color): + """ + Convert a command line colour value to a RGB triple of integers. + FIXME: Somewhere we need support for greyscale backgrounds etc. + """ + if color.startswith('#') and len(color) == 4: + return (int(color[1], 16), + int(color[2], 16), + int(color[3], 16)) + if color.startswith('#') and len(color) == 7: + return (int(color[1:3], 16), + int(color[3:5], 16), + int(color[5:7], 16)) + elif color.startswith('#') and len(color) == 13: + return (int(color[1:5], 16), + int(color[5:9], 16), + int(color[9:13], 16)) + + +def _main(argv): + """ + Run the PNG encoder with options from the command line. + """ + + # Parse command line arguments + from optparse import OptionParser + import re + version = '%prog ' + re.sub(r'( ?\$|URL: |Rev:)', '', __version__) + parser = OptionParser(version=version) + parser.set_usage("%prog [options] [imagefile]") + parser.add_option('-r', '--read-png', default=False, + action='store_true', + help='Read PNG, write PNM') + parser.add_option("-i", "--interlace", + default=False, action="store_true", + help="create an interlaced PNG file (Adam7)") + parser.add_option("-t", "--transparent", + action="store", type="string", metavar="color", + help="mark the specified colour (#RRGGBB) as transparent") + parser.add_option("-b", "--background", + action="store", type="string", metavar="color", + help="save the specified background colour") + parser.add_option("-a", "--alpha", + action="store", type="string", metavar="pgmfile", + help="alpha channel transparency (RGBA)") + parser.add_option("-g", "--gamma", + action="store", type="float", metavar="value", + help="save the specified gamma value") + parser.add_option("-c", "--compression", + action="store", type="int", metavar="level", + help="zlib compression level (0-9)") + parser.add_option("-T", "--test", + default=False, action="store_true", + help="create a test image (a named PngSuite image if an argument is supplied)") + parser.add_option('-L', '--list', + default=False, action='store_true', + help="print list of named test images") + parser.add_option("-R", "--test-red", + action="store", type="string", metavar="pattern", + help="test pattern for the red image layer") + parser.add_option("-G", "--test-green", + action="store", type="string", metavar="pattern", + help="test pattern for the green image layer") + parser.add_option("-B", "--test-blue", + action="store", type="string", metavar="pattern", + help="test pattern for the blue image layer") + parser.add_option("-A", "--test-alpha", + action="store", type="string", metavar="pattern", + help="test pattern for the alpha image layer") + parser.add_option("-K", "--test-black", + action="store", type="string", metavar="pattern", + help="test pattern for greyscale image") + parser.add_option("-d", "--test-depth", + default=8, action="store", type="int", + metavar='NBITS', + help="create test PNGs that are NBITS bits per channel") + parser.add_option("-S", "--test-size", + action="store", type="int", metavar="size", + help="width and height of the test image") + (options, args) = parser.parse_args(args=argv[1:]) + + # Convert options + if options.transparent is not None: + options.transparent = color_triple(options.transparent) + if options.background is not None: + options.background = color_triple(options.background) + + if options.list: + names = list(_pngsuite) + names.sort() + for name in names: + print name + return + + # Run regression tests + if options.test: + return test_suite(options, args) + + # Prepare input and output files + if len(args) == 0: + infilename = '-' + infile = sys.stdin + elif len(args) == 1: + infilename = args[0] + infile = open(infilename, 'rb') + else: + parser.error("more than one input file") + outfile = sys.stdout + + if options.read_png: + # Encode PNG to PPM + png = Reader(file=infile) + width,height,pixels,meta = png.asDirect() + write_pnm(outfile, width, height, pixels, meta) + else: + # Encode PNM to PNG + format, width, height, depth, maxval = \ + read_pnm_header(infile, ('P5','P6','P7')) + # When it comes to the variety of input formats, we do something + # rather rude. Observe that L, LA, RGB, RGBA are the 4 colour + # types supported by PNG and that they correspond to 1, 2, 3, 4 + # channels respectively. So we use the number of channels in + # the source image to determine which one we have. We do not + # care about TUPLTYPE. + greyscale = depth <= 2 + pamalpha = depth in (2,4) + supported = map(lambda x: 2**x-1, range(1,17)) + try: + mi = supported.index(maxval) + except ValueError: + raise NotImplementedError( + 'your maxval (%s) not in supported list %s' % + (maxval, str(supported))) + bitdepth = mi+1 + writer = Writer(width, height, + greyscale=greyscale, + bitdepth=bitdepth, + interlace=options.interlace, + transparent=options.transparent, + background=options.background, + alpha=bool(pamalpha or options.alpha), + gamma=options.gamma, + compression=options.compression) + if options.alpha: + pgmfile = open(options.alpha, 'rb') + format, awidth, aheight, adepth, amaxval = \ + read_pnm_header(pgmfile, 'P5') + if amaxval != '255': + raise NotImplementedError( + 'maxval %s not supported for alpha channel' % amaxval) + if (awidth, aheight) != (width, height): + raise ValueError("alpha channel image size mismatch" + " (%s has %sx%s but %s has %sx%s)" + % (infilename, width, height, + options.alpha, awidth, aheight)) + writer.convert_ppm_and_pgm(infile, pgmfile, outfile) + else: + writer.convert_pnm(infile, outfile) + + +if __name__ == '__main__': + try: + _main(sys.argv) + except Error, e: + print >>sys.stderr, e diff --git a/tanks/Function.py b/tanks/Function.py new file mode 100644 index 0000000..406e564 --- /dev/null +++ b/tanks/Function.py @@ -0,0 +1,46 @@ +import math + +class Function(object): + """Represents a single condition or action. This doc string is printed + as user documentation. You should override it to say something useful.""" + + def __call__(self, tank): + """The __call__ method should be of this basic form. Actions + should return None, conditions should return True or False. Actions + should utilize the set* methods of tanks. Conditions can utilize the + tanks get* methods.""" + pass + + def _limitArgs(self, args, max): + """Raises a ValueError if there are more than max args.""" + if len(args) > max: + raise ValueError("Too many arguments: %s" % ','.join(args)) + + def _checkRange(self, value, name, min=0, max=100): + """Check that the value is in the given range. + Raises an exception with useful info for invalid values. Name is used to + let the user know which value is wrong.""" + try: + value = int(value) + except: + raise ValueError("Invalid %s value: %s" % (name, value)) + assert value >= min and value <= max, "Invalid %s. %ss must be in"\ + " the %s %d-%d" % \ + (name, name.capitalize(), value, min, max) + + return value + + def _convertAngle(self, value, name): + """Parse the given value as an angle in degrees, and return its value + in radians. Raise useful errors. + Name is used in the errors to describe the field.""" + try: + angle = math.radians(value) + except: + raise ValueError("Invalid %s value: %s" % (name, value)) + + assert angle >= 0 and angle < 2*math.pi, "Invalid %s; "\ + "It be in the range 0 and 359." % name + + return angle + diff --git a/tanks/GameMath.py b/tanks/GameMath.py new file mode 100644 index 0000000..481bf81 --- /dev/null +++ b/tanks/GameMath.py @@ -0,0 +1,206 @@ +import math + +def rotatePoint(point, angle): + """Assuming 0,0 is the center, rotate the given point around it.""" + + x,y = point + r = math.sqrt(x**2 + y**2) + if r == 0: + return 0, 0 + + theta = math.acos(x/r) + if y < 0: + theta = -theta + theta = theta + angle + return int(round(r*math.cos(theta))), int(round(r*math.sin(theta))) + +def rotatePoly(points, angle): + """Rotate the given list of points around 0,0 by angle.""" + return [ rotatePoint(point, angle) for point in points ] + +def displace(point, disp, limits): + """Displace point by disp, wrapping around limits.""" + x = (point[0] + disp[0]) + while x >= limits[0]: + x = x - limits[0] + while x < 0: + x = x + limits[0] + + y = (point[1] + disp[1]) + while y >= limits[1]: + y = y - limits[1] + while y < 0: + y = y + limits[1] + + return x,y + +def displacePoly(points, disp, limits, coordSequence=False): + """Displace each point (x,y) in 'points' by 'disp' (x,y). The limits of + the drawing space are assumed to be at x=0, y=0 and x=limits[0], + y=limits[1]. If the poly overlaps the edge of the drawing space, the + poly is duplicated on each side. +@param coordSequence: If true, the coordinates are returned as a sequence - + x1, y1, x2, y2, ... This is need by some PIL drawing + commands. +@returns: A list of polys, displaced by disp + """ + xDup = 0; yDup = 0 + maxX, maxY = limits + basePoints = [] + for point in points: + x,y = int(point[0] + disp[0]), int(point[1] + disp[1]) + + # Check if duplication is needed on each axis + if x > maxX: + # If this is negative, then we need to duplicate in the negative + # direction. + xDup = -1 + elif x < 0: + xDup = 1 + + if y > maxY: + yDup = -1 + elif y < 0: + yDup = 1 + + basePoints.append( (x,y) ) + + polys = [basePoints] + if xDup: + polys.append([(x + maxX*xDup, y) for x,y in basePoints] ) + if yDup: + polys.append([(x, maxY*yDup + y) for x,y in basePoints] ) + if xDup and yDup: + polys.append([(x+maxX*xDup, maxY*yDup+y) for x,y in basePoints]) + + # Switch coordinates to sequence mode. + # (x1, y1, x2, y2) instead of ((x1, y1), (x2, y2)) + if coordSequence: + seqPolys = [] + for poly in polys: + points = [] + for point in poly: + points.extend(point) + seqPolys.append(points) + polys = seqPolys + + return polys + +def polar2cart(r, theta): + """Return the cartesian coordinates for r, theta.""" + x = r*math.cos(theta) + y = r*math.sin(theta) + return x,y + +def minShift(center, point, limits): + """Get the minimum distances between the two points, given that the board + wraps at the givin limits.""" + dx = point[0] - center[0] + if dx < -limits[0]/2.0: + dx = point[0] + limits[0] - center[0] + elif dx > limits[0]/2.0: + dx = point[0] - (center[0] + limits[0]) + + dy = point[1] - center[1] + if dy < - limits[1]/2.0: + dy = point[1] + limits[1] - center[1] + elif dy > limits[1]/2.0: + dy = point[1] - (limits[1] + center[1]) + + return dx, dy + +def relativePolar(center, point, limits): + """Returns the angle, from zero, to the given point assuming this +center is the origin. Take into account wrapping round the limits of the board. +@returns: r, theta + """ + + dx, dy = minShift(center, point, limits) + + r = math.sqrt(dx**2 + dy**2) + theta = math.acos(dx/r) + if dy < 0: + theta = 2*math.pi - theta + + return r, theta + +def reduceAngle(angle): + """Reduce the angle such that it is in 0 <= angle < 2pi""" + + while angle >= math.pi*2: + angle = angle - math.pi*2 + while angle < 0: + angle = angle + math.pi*2 + + return angle + +def angleDiff(angle1, angle2): + """Returns the difference between the two angles. They are assumed +to be in radians, and must be in the range 0 <= angle < 2*pi. +@raises AssertionError: The angles given must be in the range 0 <= angle < 2pi +@returns: The minimum distance between the two angles; The distance + is negative if angle2 leads angle1 (clockwise).. + """ + + for angle in angle1, angle2: + assert angle < 2*math.pi and angle >= 0, \ + 'angleDiff: bad angle %s' % angle + + diff = angle2 - angle1 + if diff > math.pi: + diff = diff - 2*math.pi + elif diff < -math.pi: + diff = diff + 2*math.pi + + return diff + +def getDist(point1, point2): + """Returns the distance between point1 and point2.""" + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + + return math.sqrt(dx**2 + dy**2) + +def segmentCircleCollision(segment, center, radius): + """Return True if the given circle touches the given line segment. +@param segment: A list of two points [(x1,y1), (x2, y2)] that define + the line segment. +@param center: The center point of the circle. +@param radius: The radius of the circle. +@returns: True if the the circle touches the line segment, False otherwise. + """ + + a = getDist(segment[0], center) + c = getDist(segment[1], center) + base = getDist(segment[0], segment[1]) + + # If we're close enough to the end points, then we're close + # enough to the segment. + if a < radius or c < radius: + return True + + # First we find the are of the triangle formed by the line segment + # and point. I use Heron's formula for the area. Using this, we'll + # find the distance d from the point to the line. We'll later make + # sure that the collision is with the line segment, and not just the + # line. + s = (a + c + base)/2 + A = math.sqrt(s*(s - a)*(s - c)*(s - base)) + d = 2*A/base + +# print s, a, c, A, d, radius + + # If the distance from the point to the line is more than the + # target radius, this isn't a hit. + if d > radius: + return False + + # If the distance from an endpoint to the intersection between + # our line segment and the line perpendicular to it that passes through + # the point is longer than the line segment, then this isn't a hit. + elif math.sqrt(a**2 - d**2) > base or \ + math.sqrt(c**2 - d**2) > base: + return False + else: + # The triangle is acute, that means we're close enough. + return True diff --git a/tanks/Pflanzarr.py b/tanks/Pflanzarr.py new file mode 100644 index 0000000..18a54fe --- /dev/null +++ b/tanks/Pflanzarr.py @@ -0,0 +1,399 @@ +import fcntl +import math +import os +import random +import cgi +from sets import Set as set +from ctf import teams, html, paths +from cStringIO import StringIO + +from urllib import unquote, quote + +import Tank + +class NotEnoughPlayers(Exception): + pass + +class Pflanzarr: + SPACING = 150 + + def __init__(self, dir): + """Initialize a new game of Pflanzarr. +@param dir: The data directory.""" + + # Setup the game environment + self._setupDirectories(dir) + + # Figure out what game number this is. + self.gameNum = self._getGameNum() + self.gameFilename = os.path.join(self._resultsDir, '%04d.html' % self.gameNum) + + tmpPlayers = os.listdir(self._playerDir) + players = [] + for p in tmpPlayers: + p = unquote(p) + if (not (p.startswith('.') + or p.endswith('#') + or p.endswith('~')) + and teams.exists(p)): + players.append(p) + + AIs = {} + for player in players: + AIs[player] = open(os.path.join(self._playerDir, player)).read() + defaultAIs = self._getDefaultAIs(dir) + + if len(players) < 1: + raise NotEnoughPlayers() + + # The one is added to ensure that there is at least one house + # bot. + cols = math.sqrt(len(players) + 1) + if int(cols) != cols: + cols = cols + 1 + + cols = int(cols) + cols = max(cols, 2) + + rows = len(players)/cols + if len(players) % cols != 0: + rows = rows + 1 + rows = max(rows, 2) + + self._board = (cols*self.SPACING, rows*self.SPACING) + + while len(players) < cols*rows: + players.append(None) + + self._tanks = [] + for i in range(cols): + for j in range(rows): + startX = i*self.SPACING + self.SPACING/2 + startY = j*self.SPACING + self.SPACING/2 + player = random.choice(players) + players.remove(player) + color = '#' + teams.color(player) + tank = Tank.Tank( player, (startX, startY), color, + self._board, testMode=True) + if player == None: + tank.program(random.choice(defaultAIs)) + else: + tank.program(AIs[player]) + self._tanks.append(tank) + + # We only want to make these once, so we do it here. + self._tanksByX = list(self._tanks) + self._tanksByY = list(self._tanks) + + self._deadTanks = set() + + def run(self, maxTurns=None): + kills = {} + for tank in self._tanks: + kills[tank] = set() + + # Open HTML output + hdr = StringIO() + hdr.write('\n' + '\n') + + # Decide on the winner + winner = self._chooseWinner(kills) + self.winner = winner.name + + # Now generate HTML body + body = StringIO() + body.write(' \n' % self._board) + body.write(' Sorry, you need an HTML5-capable browser to see this.\n' + ' \n' + '

\n') + if self.gameNum > 0: + body.write(' ← Prev |' % + (self.gameNum - 1)) + body.write(' Next → |' % + (self.gameNum + 1)) + body.write(' 0 fps\n' + '

\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n') + + tanks = self._tanks[:] + tanks.remove(winner) + tanks[0:0] = [winner] + for tank in tanks: + if tank is winner: + rowStyle = ('style="font-weight: bold; ' + 'color: #000; ' + 'background-color: %s;"' % tank.color) + else: + rowStyle = 'style="background-color:%s; color: #000;"' % tank.color + if tank.name: + name = cgi.escape(tank.name) + else: + name = teams.house + body.write('' % + (rowStyle, + name, + len(kills[tank]), + cgi.escape(tank.deathReason))) + body.write('
TeamKillsCause of Death
%s%d%s
\n') + + # Write everything out + html.write(self.gameFilename, + 'Tanks round %d' % self.gameNum, + body.getvalue(), + hdr=hdr.getvalue(), + onload='start(turns);') + + + + def _killTanks(self, tanks, reason): + for tank in tanks: + if tank in self._tanksByX: + self._tanksByX.remove(tank) + if tank in self._tanksByY: + self._tanksByY.remove(tank) + + tank.die(reason) + + self._deadTanks = self._deadTanks.union(tanks) + + def _chooseWinner(self, kills): + """Choose a winner. In case of a tie, live tanks prevail, in case + of further ties, a winner is chosen at random. This outputs the winner + to the winners file and outputs a results table html file.""" + tanks = list(self._tanks) + def winSort(t1, t2): + """Sort by # of kill first, then by life status.""" + result = cmp(len(kills[t1]), len(kills[t2])) + if result != 0: + return result + + if t1.isDead and not t2.isDead: + return -1 + elif not t1.isDead and t2.isDead: + return 1 + else: + return 0 + tanks.sort(winSort) + tanks.reverse() + + # Get the list of potential winners + winners = [] + for i in range(len(tanks)): + if len( kills[tanks[0]] ) == len( kills[tanks[i]] ) and \ + tanks[0].isDead == tanks[i].isDead: + winners.append(tanks[i]) + else: + break + winner = random.choice(winners) + return winner + + + def _outputErrors(self, tank): + """Output errors for each team.""" + if tank.name == None: + return + + if tank._program.errors: + print tank.name, 'has errors' + + + fileName = os.path.join(self._errorDir, quote(tank.name, '')) + file = open(fileName, 'w') + for error in tank._program.errors: + file.write(error) + file.write('\n') + file.close() + + def _getNear(self): + """A dictionary of the set of tanks nearby each tank. Nearby is + defined as within the square centered the tank with side length equal + twice the sensor range. Only a few tanks within the set (those in the + corners of the square) should be outside the sensor range.""" + + self._tanksByX.sort(lambda t1, t2: cmp(t1.pos[0], t2.pos[0])) + self._tanksByY.sort(lambda t1, t2: cmp(t1.pos[1], t2.pos[1])) + + nearX = {} + nearY = {} + for tank in self._tanksByX: + nearX[tank] = set() + nearY[tank] = set() + + numTanks = len(self._tanksByX) + offset = 1 + for index in range(numTanks): + cTank = self._tanksByX[index] + maxRange = cTank.SENSOR_RANGE + cTank.RADIUS + 1 + near = set([cTank]) + for i in [(j + index) % numTanks for j in range(1, offset)]: + near.add(self._tanksByX[i]) + while offset < numTanks: + nTank = self._tanksByX[(index + offset) % numTanks] + if (index + offset >= numTanks and + self._board[0] + nTank.pos[0] - cTank.pos[0] < maxRange): + near.add(nTank) + offset = offset + 1 + elif (index + offset < numTanks and + nTank.pos[0] - cTank.pos[0] < maxRange ): + near.add(nTank) + offset = offset + 1 + else: + break + + if offset > 1: + offset = offset - 1 + + for tank in near: + nearX[tank] = nearX[tank].union(near) + + offset = 1 + for index in range(numTanks): + cTank = self._tanksByY[index] + maxRange = cTank.SENSOR_RANGE + cTank.RADIUS + 1 + near = set([cTank]) + for i in [(j + index) % numTanks for j in range(1, offset)]: + near.add(self._tanksByY[i]) + while offset < numTanks: + nTank = self._tanksByY[(index + offset) % numTanks] + if (index + offset < numTanks and + nTank.pos[1] - cTank.pos[1] < maxRange): + near.add(nTank) + offset = offset + 1 + elif (index + offset >= numTanks and + self._board[1] + nTank.pos[1] - cTank.pos[1] < maxRange): + near.add(nTank) + offset = offset + 1 + else: + break + + if offset > 1: + offset = offset - 1 + + for tank in near: + nearY[tank] = nearY[tank].union(near) + + near = {} + for tank in self._tanksByX: + near[tank] = nearX[tank].intersection(nearY[tank]) + near[tank].remove(tank) + + return near + + def _setupDirectories(self, dir): + """Setup all the directories needed by the game.""" + + if not os.path.exists(dir): + os.mkdir(dir) + + self._dir = dir + + # Don't run more than one game at the same time. + self._lockFile = open(os.path.join(dir, '.lock'), 'a') + try: + fcntl.flock(self._lockFile, fcntl.LOCK_EX|fcntl.LOCK_NB) + except: + sys.exit(1) + + # Setup all the directories we'll need. + self._resultsDir = os.path.join(dir, 'results') + self._errorDir = os.path.join(dir, 'errors') + self._playerDir = os.path.join(dir, 'ai', 'players') + + def _getDefaultAIs(self, basedir): + """Load all the house bot AIs.""" + defaultAIs = [] + + path = os.path.join(basedir, 'ai', 'house') + files = os.listdir(path) + for fn in files: + if fn.startswith('.'): + continue + + fn = os.path.join(path, fn) + file = open(fn) + defaultAIs.append(file.read()) + + return defaultAIs + + def _getGameNum(self): + """Figure out what game number this is from the past games played.""" + + games = os.listdir(self._resultsDir) + games.sort() + if games: + fn = games[-1] + s, _ = os.path.splitext(fn) + return int(s) + 1 + else: + return 0 + +if __name__ == '__main__': + import sys, traceback + try: + p = Pflanzarr(sys.argv[1]) + p.run(int(sys.argv[3])) + except: + traceback.print_exc() + print "Usage: Pflanzarr.py dataDirectory #turns" + + diff --git a/tanks/Program.py b/tanks/Program.py new file mode 100644 index 0000000..cab13ab --- /dev/null +++ b/tanks/Program.py @@ -0,0 +1,234 @@ +"""

Introduction

+You are the proud new operator of a M-375 Pflanzarr Tank. Your tank is +equipped with a powerful laser cannon, independently rotating turret +section, up to 10 enemy detection sensors, and a standard issue NATO hull. +Unfortunately, it lacks seats, and thus must rely own its own wits and your +skills at designing those wits to survive. + +

Programming Your Tank

+Your tanks are programmed using the Super Useful Command and Kontrol language, +the very best in laser tank AI languages. It includes amazing features such +as comments (Started by a #, ended at EOL), logic, versatility, and +semi-colons (all lines must end in one). As with all new military systems +it utilizes only integers; we must never rest in our +diligence against the communist floating point conspiracy. Whitespace is +provided by trusted contractors, and should never interfere with operations. +

+Your program should be separated into Setup and AI commands. The definitions +section lets you designated the behaviors of its sensors and memory. +Each setup command must begin with a '>'. Placing setup commands after +the first AI command is a violation of protocol. +Here are some examples of correct setup commands: +

>addsensor(80, 90, 33);
+>addsensor(50, 0, 10, 1);
+>addtimer(3);
+ +The AI section will act as the brain of your tank. Each AI line is +separated into a group of conditions functions and a group of action +functions. If all the conditions are satisfactory (true), all of the actions +are given as orders. Conditions are separated by ampersands, actions separated +by periods. Here are some examples of AI commands: +
+sensor(1) & sensor(2) & fireready() : fire();
+sensor(0,0)&sin(5): move(40, 30) . turretcw(50);
+sensor(4) & random(4,5) : led(1).settoggle(0,1);
+ +Your tank will check its program each turn, and attempt to the best of its +abilities to carry out its orders (or die trying). Like any military mind, +your tank may receive a plethora of often conflicting orders and information. +This a SMART TANK, however. It knows that the proper thing to do with each +subsystem is to have that subsystem follow only the last order given each turn. +""" + +import traceback +import conditions +import actions +import setup + +class Statement(object): + """Represents a single program statement. If all the condition Functions + evaluate to True, the actions are all executed in order.""" + + def __init__(self, lineNum, line, conditions, actions): + self.lineNum = lineNum + self.line = line + self._conditions = conditions + self._actions = actions + + def __call__(self, tank): + success = True + for condition in self._conditions: + if not condition(tank): + success = False + break + + if success: + for action in self._actions: + action(tank) + +class Program(object): + """This parses and represents a Tank program.""" + CONDITION_SEP = '&' + ACTION_SEP = '.' + + def __init__(self, text): + """Initialize this program, parsing the given text.""" + self.errors = [] + + self._program, self._setup = self._parse(text) + + def setup(self, tank): + """Execute all the setup actions.""" + for action in self._setup: + try: + action(tank) + except Exception, msg: + self.errors.append("Bad setup action, line %d, msg: %s" % \ + (action.lineNum, msg)) + + def __call__(self, tank): + """Execute this program on the given tank.""" + for statement in self._program: + try: + statement(tank) + except Exception, msg: + traceback.print_exc() + self.errors.append('Error executing program. \n' + '(%d) - %s\n' + 'msg: %s\n' % + (statement.lineNum, statement.line, msg) ) + + def _parse(self, text): + """Parse the text of the given program.""" + program = [] + setup = [] + inSetup = True + lines = text.split(';') + lineNum = 0 + for line in lines: + lineNum = lineNum + 1 + + originalLine = line + + # Remove Comments + parts = line.split('\n') + for i in range(len(parts)): + comment = parts[i].find('#') + if comment != -1: + parts[i] = parts[i][:comment] + # Remove all whitespace + line = ''.join(parts) + line = line.replace('\r', '') + line = line.replace('\t', '') + line = line.replace(' ', '') + + if line == '': + continue + + if line.startswith('>'): + if inSetup: + if '>' in line[1:] or ':' in line: + self.errors.append('(%d) Missing semicolon: %s' % + (lineNum, line)) + continue + + try: + setupAction = self._parseSection(line[1:], 'setup')[0] + setupAction.lineNum = lineNum + setup.append(setupAction) + except Exception, msg: + self.errors.append('(%d) Error parsing setup line: %s' + '\nThe error was: %s' % + (lineNum, originalLine, msg)) + + continue + else: + self.errors.append('(%d) Setup lines aren\'t allowed ' + 'after the first command: %s' % + (lineNum, originalLine)) + else: + # We've hit the first non-blank, non-comment, non-setup + # line + inSetup = False + + semicolons = line.count(':') + if semicolons > 1: + self.errors.append('(%d) Missing semicolon: %s' % + (lineNum, line)) + continue + elif semicolons == 1: + conditions, actions = line.split(':') + else: + self.errors.append('(%d) Invalid Line, no ":" seperator: %s'% + (lineNum, line) ) + + try: + conditions = self._parseSection(conditions, 'condition') + except Exception, msg: + self.errors.append('(%d) %s - "%s"' % + (lineNum, msg, line) ) + continue + + try: + actions = self._parseSection(actions, 'action') + except Exception, msg: + self.errors.append('(%d) %s - "%s"' % + (lineNum, msg, originalLine) ) + continue + program.append(Statement(lineNum, line, conditions, actions)) + + return program, setup + + def _parseSection(self, section, sectionType): + """Parses either the action or condition section of each command. +@param section: The text of the section of the command to be parsed. +@param sectionType: The type of section to be parsed. Should be: + 'condition', 'action', or 'setup'. +@raises ValueError: Raises ValueErrors when parsing errors occur. +@returns: Returns a list of parsed section components (Function objects). + """ + + if sectionType == 'condition': + parts = section.split(self.CONDITION_SEP) + functions = conditions.conditions + if section == '': + return [] + elif sectionType == 'action': + parts = section.split(self.ACTION_SEP) + functions = actions.actions + if section == '': + raise ValueError("The action section cannot be empty.") + elif sectionType == 'setup': + parts = [section] + functions = setup.setup + else: + raise ValueError('Invalid section Type - Contact Contest Admin') + + parsed = [] + for part in parts: + + pos = part.find('(') + if pos == -1: + raise ValueError("Missing open paren in %s: %s" % + (sectionType, part) ) + funcName = part[:pos] + + if funcName not in functions: + raise ValueError("%s function %s is not accepted." % + (sectionType.capitalize(), funcName) ) + + if part[-1] != ')': + raise ValueError("Missing closing paren in %s: %s" % + (condition, sectionType) ) + + args = part[pos+1:-1] + if args != '': + args = args.split(',') + for i in range(len(args)): + args[i] = int(args[i]) + else: + args = [] + + parsed.append(functions[funcName](*args)) + + return parsed diff --git a/tanks/Tank.py b/tanks/Tank.py new file mode 100644 index 0000000..1c9b32e --- /dev/null +++ b/tanks/Tank.py @@ -0,0 +1,479 @@ +import math +import random +from sets import Set as set + +import GameMath as gm +import Program + +class Tank(object): + + # How often, in turns, that we can fire. + FIRE_RATE = 20 + # How far the laser shoots from the center of the tank + FIRE_RANGE = 45.0 + # The radius of the tank, from the center of the turret. + # This is what is used for collision and hit detection. + RADIUS = 7.5 + # Max speed, in pixels + SPEED = 7.0 + # Max acceleration, as a fraction of speed. + ACCEL = 35 + # Sensor range, in pixels + SENSOR_RANGE = 90.0 + # Max turret turn rate, in radians + TURRET_TURN_RATE = math.pi/10 + + # The max number of sensors/timers/toggles + SENSOR_LIMIT = 10 + + def __init__(self, name, pos, color, boardSize, angle=None, tAngle=None, + testMode=True): + """Create a new tank. +@param name: The name name of the tank. Stored in self.name. +@param pos: The starting position of the tank (x,y) +@param color: The color of the tank. +@param boardSize: The size of the board. (maxX, maxY) +@param angle: The starting angle of the tank, defaults to random. +@param tAngle: The starting turretAngle of the tank, defaults to random. +@param testMode: When True, extra debugging information is displayed. Namely, + arcs for each sensor are drawn, which turn white when + activated. + """ + + # Keep track of what turn number it is for this tank. + self._turn = 0 + + self.name = name + self._testMode = testMode + + assert len(pos) == 2 and pos[0] > 0 and pos[1] > 0, \ + 'Bad starting position: %s' % str(pos) + self.pos = pos + + # The last speed of each tread (left, right) + self._lastSpeed = 0.0, 0.0 + # The next speed that the tank should try to attain. + self._nextMove = 0,0 + + # When set, the led is drawn on the tank. + self.led = False + + assert len(boardSize) == 2 and boardSize[0] > 0 and boardSize[1] > 0 + # The limits of the playfield (maxX, maxY) + self._limits = boardSize + + # The current angle of the tank. + if angle is None: + self._angle = random.random()*2*math.pi + else: + self._angle = angle + + # The current angle of the turret + if tAngle is None: + self._tAngle = random.random()*2*math.pi + else: + self._tAngle = tAngle + + self.color = color + + # You can't fire until fireReady is 0. + self._fireReady = self.FIRE_RATE + # Means the tank will fire at it's next opportunity. + self._fireNow = False + # True when the tank has fired this turn (for drawing purposes) + self._fired = False + + # What the desired turret angle should be (from the front of the tank). + # None means the turret should stay stationary. + self._tGoal = None + + # Holds the properties of each sensor + self._sensors = [] + # Holds the state of each sensor + self._sensorState = [] + + # The tanks toggle memory + self.toggles = [] + + # The tanks timers + self._timers = [] + + # Is this tank dead? + self.isDead = False + # The frame of the death animation. + self._deadFrame = 10 + # Death reason + self.deathReason = 'survived' + + def __repr__(self): + return '' % (self.name, self.pos[0], self.pos[1]) + + def get_turn(self): + return self._turn + turn = property(get_turn) + + def fire(self, near): + """Shoots, if ordered to and able. Returns the set of tanks + destroyed.""" + + killed = set() + if self._fireReady > 0: + # Ignore the shoot order + self._fireNow = False + + if self._fireNow: + self._fireNow = False + self._fireReady = self.FIRE_RATE + self._fired = True + + + firePoint = gm.polar2cart(self.FIRE_RANGE, + self._angle + self._tAngle) + for tank in near: + enemyPos = gm.minShift(self.pos, tank.pos, self._limits) + if gm.segmentCircleCollision(((0,0), firePoint), enemyPos, + self.RADIUS): + killed.add(tank) + else: + self._fired = False + + return killed + + def addSensor(self, range, angle, width, attachedTurret=False): + """Add a sensor to this tank. +@param angle: The angle, from the tanks front and going clockwise, + of the center of the sensor. (radians) +@param width: The width of the sensor (percent). +@param range: The range of the sensor (percent) +@param attachedTurret: If set, the sensor moves with the turret. + """ + assert range >=0 and range <= 1, "Invalid range value." + + if len(self._sensors) >= self.SENSOR_LIMIT: + raise ValueError("You can only have 10 sensors.") + + range = range * self.SENSOR_RANGE + + if attachedTurret: + attachedTurret = True + else: + attachedTurret = False + + self._sensors.append((range, angle, width, attachedTurret)) + self._sensorState.append(False) + + def getSensorState(self, key): + return self._sensorState[key] + + def setMove(self, left, right): + """Parse the speed values given, and set them for the next move.""" + + self._nextMove = left, right + + def setTurretAngle(self, angle=None): + """Set the desired angle of the turret. No angle means the turret + should remain stationary.""" + + if angle is None: + self._tGoal = None + else: + self._tGoal = gm.reduceAngle(angle) + + def setFire(self): + """Set the tank to fire, next turn.""" + self._fireNow = True + + def fireReady(self): + """Returns True if the tank can fire now.""" + return self._fireReady == 0 + + def addTimer(self, period): + """Add a timer with timeout period 'period'.""" + + if len(self._timers) >= self.SENSOR_LIMIT: + raise ValueError('You can only have 10 timers') + + self._timers.append(None) + self._timerPeriods.append(period) + + def resetTimer(self, key): + """Reset, and start the given timer, but only if it is inactive. + If it is active, raise a ValueError.""" + if self._timer[key] is None: + self._timer[key] = self._timerPeriods[key] + else: + raise ValueError("You can't reset an active timer (#%d)" % key) + + def clearTimer(self, key): + """Clear the timer.""" + self._timer[key] = None + + def checkTimer(self, key): + """Returns True if the timer has expired.""" + return self._timer[key] == 0 + + def _manageTimers(self): + """Decrement each active timer.""" + for i in range(len(self._timers)): + if self._timers[i] is not None and \ + self._timers[i] > 0: + self._timers[i] = self._timers[i] - 1 + + def program(self, text): + """Set the program for this tank.""" + + self._program = Program.Program(text) + self._program.setup(self) + + def execute(self): + """Execute this tanks program.""" + + # Decrement the active timers + self._manageTimers() + self.led = False + + self._program(self) + + self._move(self._nextMove[0], self._nextMove[1]) + self._moveTurret() + if self._fireReady > 0: + self._fireReady = self._fireReady - 1 + + self._turn = self._turn + 1 + + def sense(self, near): + """Detect collisions and trigger sensors. Returns the set of + tanks collided with, plus this one. We do both these steps at once + mainly because all the data is available.""" + + near = list(near) + polar = [] + for tank in near: + polar.append(gm.relativePolar(self.pos, tank.pos, self._limits)) + + for sensorId in range(len(self._sensors)): + # Reset the sensor + self._sensorState[sensorId] = False + + dist, sensorAngle, width, tSens = self._sensors[sensorId] + + # Adjust the sensor angles according to the tanks angles. + sensorAngle = sensorAngle + self._angle + # If the angle is tied to the turret, add that too. + if tSens: + sensorAngle = sensorAngle + self._tAngle + + while sensorAngle >= 2*math.pi: + sensorAngle = sensorAngle - 2*math.pi + + for i in range(len(near)): + r, theta = polar[i] + # Find the difference between the object angle and the sensor. + # The max this can be is pi, so adjust for that. + dAngle = gm.angleDiff(theta, sensorAngle) + + rCoord = gm.polar2cart(dist, sensorAngle - width/2) + lCoord = gm.polar2cart(dist, sensorAngle + width/2) + rightLine = ((0,0), rCoord) + leftLine = ((0,0), lCoord) + tankRelPos = gm.minShift(self.pos, near[i].pos, self._limits) + if r < (dist + self.RADIUS): + if abs(dAngle) <= (width/2) or \ + gm.segmentCircleCollision(rightLine, tankRelPos, + self.RADIUS) or \ + gm.segmentCircleCollision(leftLine, tankRelPos, + self.RADIUS): + + self._sensorState[sensorId] = True + break + + # Check for collisions here, since we already have all the data. + collided = set() + for i in range(len(near)): + r, theta = polar[i] + if r < (self.RADIUS + near[i].RADIUS): + collided.add(near[i]) + + # Add this tank (a collision kills both, after all + if collided: + collided.add(self) + + return collided + + def die(self, reason): + self.isDead = True + self.deathReason = reason + + def _moveTurret(self): + if self._tGoal is None or self._tAngle == self._tGoal: + return + + diff = gm.angleDiff(self._tGoal, self._tAngle) + + if abs(diff) < self.TURRET_TURN_RATE: + self._tAngle = self._tGoal + elif diff > 0: + self._tAngle = gm.reduceAngle(self._tAngle - self.TURRET_TURN_RATE) + else: + self._tAngle = gm.reduceAngle(self._tAngle + self.TURRET_TURN_RATE) + + def _move(self, lSpeed, rSpeed): + + assert abs(lSpeed) <= 100, "Bad speed value: %s" % lSpeed + assert abs(rSpeed) <= 100, "Bad speed value: %s" % rSpeed + + # Handle acceleration + if self._lastSpeed[0] < lSpeed and \ + self._lastSpeed[0] + self.ACCEL < lSpeed: + lSpeed = self._lastSpeed[0] + self.ACCEL + elif self._lastSpeed[0] > lSpeed and \ + self._lastSpeed[0] - self.ACCEL > lSpeed: + lSpeed = self._lastSpeed[0] - self.ACCEL + + if self._lastSpeed[1] < rSpeed and \ + self._lastSpeed[1] + self.ACCEL < rSpeed: + rSpeed = self._lastSpeed[1] + self.ACCEL + elif self._lastSpeed[1] > rSpeed and \ + self._lastSpeed[1] - self.ACCEL > rSpeed: + rSpeed = self._lastSpeed[1] - self.ACCEL + + self._lastSpeed = lSpeed, rSpeed + + # The simple case + if lSpeed == rSpeed: + fSpeed = self.SPEED*lSpeed/100 + x = fSpeed*math.cos(self._angle) + y = fSpeed*math.sin(self._angle) + # Adjust our position + self._reposition((x,y), 0) + return + + # The works as follows: + # The tank drives around in a circle of radius r, which is some + # offset on a line perpendicular to the tank. The distance it travels + # around the circle varies with the speed of each tread, and is + # such that each side of the tank moves an equal angle around the + # circle. + L = self.SPEED * lSpeed/100.0 + R = self.SPEED * rSpeed/100.0 + friction = .75 * abs(L-R)/(2.0*self.SPEED) + L = L * (1 - friction) + R = R * (1 - friction) + + # Si is the speed of the tread on the inside of the turn, + # So is the speed on the outside of the turn. + # dir is to note the direction of rotation. + if abs(L) > abs(R): + Si = R; So = L + dir = 1 + else: + Si = L; So = R + dir = -1 + + # The width of the tank... + w = self.RADIUS * 2 + + # This is the angle that will determine the circle the tank travels + # around. +# theta = math.atan((So - Sl)/w) + # This is the distance from the outer tread to the center of the + # circle formed by it's movement. + r = w*So/(So - Si) + + # The fraction of the circle traveled is equal to the speed of + # the outer tread over the circumference of the circle. + # Ft = So/(2*pi*r) + # The angle traveled is equal to the Fraction traveled * 2 * pi + # This reduces to a simple: So/r + # We multiply it by dir to adjust for the direction of rotation + theta = So/r * dir + + # These are the offsets from the center of the circle, given that + # the tank is turned in some direction. The tank is facing + # perpendicular to the circle + # So far everything has been relative to the outer tread. At this + # point, however, we need to move relative to the center of the + # tank. Hence the adjustment in r. + x = -math.cos( self._angle + math.pi/2*dir ) * (r - w/2.0) + y = -math.sin( self._angle + math.pi/2*dir ) * (r - w/2.0) + + # Now we just rotate the tank's position around the center of the + # circle to get the change in coordinates. + mx, my = gm.rotatePoint((x,y), theta) + mx = mx - x + my = my - y + + # Finally, we shift the tank relative to the playing field, and + # rotate it by theta. + self._reposition((mx, my), theta) + + def _reposition(self, move, angleChange): + """Move the tank by x,y = move, and change it's angle by angle. + I assume the tanks move slower than the boardSize.""" + + x = self.pos[0] + move[0] + y = self.pos[1] + move[1] + self._angle = self._angle + angleChange + + if x < 0: + x = self._limits[0] + x + elif x > self._limits[0]: + x = x - self._limits[0] + + if y < 0: + y = self._limits[1] + y + elif y > self._limits[1]: + y = y - self._limits[1] + + self.pos = round(x), round(y) + + while self._angle < 0: + self._angle = self._angle + math.pi * 2 + + while self._angle > math.pi * 2: + self._angle = self._angle - math.pi * 2 + + def draw(self, f): + """Output this tank's state as JSON. + + [color, x, y, angle, turret_angle, led, fired] + + """ + + f.write(' [') + f.write(str(int(self.isDead))); + f.write(',') + f.write(repr(self.color)) + f.write(',') + f.write('%d' % self.pos[0]) + f.write(',') + f.write('%d' % self.pos[1]) + f.write(',') + f.write('%.2f' % self._angle) + f.write(',') + f.write('%.2f' % self._tAngle) + f.write(',') + f.write(str(int(self.led))) + f.write(',') + f.write('%d' % (self._fired and self.FIRE_RANGE) or 0) + if not self.isDead: + f.write(',[') + for i in range(len(self._sensors)): + dist, sensorAngle, width, tSens = self._sensors[i] + + # If the angle is tied to the turret, add that. + if tSens: + sensorAngle = sensorAngle + self._tAngle + + f.write('[') + f.write(str(int(dist))) + f.write(',') + f.write('%.2f' % (sensorAngle - width/2)); + f.write(',') + f.write('%.2f' % (sensorAngle + width/2)); + f.write(',') + f.write(str(int(self._sensorState[i]))) + f.write('],') + f.write(']') + + f.write('],\n') diff --git a/tanks/__init__.py b/tanks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tanks/actions.py b/tanks/actions.py new file mode 100644 index 0000000..a03e7af --- /dev/null +++ b/tanks/actions.py @@ -0,0 +1,126 @@ +"""Define new action Functions here. They should inherit from the +Function.Function class. To make an action usable, add it to the +actions dictionary at the end of this file.""" + +import Function + +class Move(Function.Function): + """move(left tread speed, right tread speed) + Set the speeds for the tanks right and left treads. The speeds should + be a number (percent power) between -100 and 100.""" + + def __init__(self, left, right): + self._checkRange(left, 'left tread speed', min=-100) + self._checkRange(right, 'right tread speed', min=-100) + + self._left = left + self._right = right + + def __call__(self, tank): + tank.setMove(self._left, self._right) + +class TurretCounterClockwise(Function.Function): + """turretccw([percent speed]) + Rotate the turret counter clockwise as a percentage of the max speed.""" + def __init__(self, speed=100): + self._checkRange(speed, 'turret percent speed') + self._speed = speed/100.0 + def __call__(self, tank): + tank.setTurretAngle(tank._tAngle - tank.TURRET_TURN_RATE*self._speed) + +class TurretClockwise(Function.Function): + """turretcw([percent speed]) + Rotate the turret clockwise at a rate preportional to speed.""" + + def __init__(self, speed=100): + self._checkRange(speed, 'turret percent speed') + self._speed = speed/100.0 + def __call__(self, tank): + tank.setTurretAngle(tank._tAngle + tank.TURRET_TURN_RATE*self._speed) + +class TurretSet(Function.Function): + """turretset([angle]) + Set the turret to the given angle, in degrees, relative to the front of + the tank. Angles increase counterclockwise. + The angle can be left out; in that case, this locks the turret + to it's current position.""" + + def __init__(self, angle=None): + # Convert the angle to radians + if angle is not None: + angle = self._convertAngle(angle, 'turret angle') + + self._angle = angle + + def __call__(self, tank): + tank.setTurretAngle(self._angle) + +class Fire(Function.Function): + """fire() + Attempt to fire the tanks laser cannon.""" + + def __call__(self, tank): + tank.setFire() + +class SetToggle(Function.Function): + """settoggle(key, state) +Set toggle 'key' to 'state'. +""" + def __init__(self, key, state): + self._key = key + self._state = state + def __call__(self, tank): + tank.toggles[self._key] = self._state + +class Toggle(Function.Function): + """toggle(key) +Toggle the value of toggle 'key'. +""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + try: + tank.toggles[self._key] = not tank.toggles[self._key] + except IndexError: + raise IndexError('Invalid toggle: %d' % self._key) + +class LED(Function.Function): + """led(state) +Set the tanks LED to state (true is on, false is off). +The led is a light that appears behind the tanks turret. +It remains on for a single turn.""" + def __init__(self, state=1): + self._state = state + def __call__(self, tank): + tank.led = self._state + +class StartTimer(Function.Function): + """starttimer(#) +Start (and reset) the given timer, but only if it is inactive. +""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.resetTimer(key) + +class ClearTimer(Function.Function): + """cleartimer(#) +Clear the given timer such that it is no longer active (inactive timers +are always False).""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.clearTimer(self._key) + +### When adding names to this dict, make sure they are lower case and alpha +### numeric. +actions = {'move': Move, + 'turretccw': TurretCounterClockwise, + 'turretcw': TurretClockwise, + 'turretset': TurretSet, + 'fire': Fire, + 'settoggle': SetToggle, + 'toggle': Toggle, + 'led': LED, + 'starttimer': StartTimer, + 'cleartimer': ClearTimer} diff --git a/tanks/conditions.py b/tanks/conditions.py new file mode 100644 index 0000000..1401d54 --- /dev/null +++ b/tanks/conditions.py @@ -0,0 +1,126 @@ +"""Define new condition functions here. Add it to the conditions dictionary +at the end to make it usable by Program.Program. These should inherit from +Function.Function.""" + +import Function +import math +import random + +class Sense(Function.Function): + """sense(#, [invert]) + Takes a Sensor number as an argument. + Returns True if the given sensor is currently activated, False otherwise. + If the option argument invert is set to true then logic is inverted, + and then sensor returns True when it is NOT activated, and False when + it is. Invert is false by default. + """ + + def __init__(self, sensor, invert=0): + self._sensor = sensor + self._invert = invert + + def __call__(self, tank): + state = tank.getSensorState(self._sensor) + if self._invert: + return not state + else: + return state + +class Toggle(Function.Function): + """toggle(#) +Returns True if the given toggle is set, False otherwise. """ + def __init__(self, toggle): + self._toggle = toggle + def __call__(self, tank): + return tank.toggles[toggle] + +class TimerCheck(Function.Function): + """timer(#, [invert]) +Checks the state of timer # 'key'. Returns True if time has run out. +If invert is given (and true), then True is returned if the timer has +yet to expire. +""" + def __init__(self, key, invert=0): + self._key = key + self._invert = invert + def __call__(self, tank): + state = tank.checkTimer(self._key) + if invert: + return not state + else: + return state + +class Random(Function.Function): + """random(n,m) + Takes two arguments, n and m. Generates a random number between 1 + and m (inclusive) each time it's checked. If the random number is less + than or equal + to n, then the condition returns True. Returns False otherwise.""" + + def __init__(self, n, m): + self._n = n + self._m = m + + def __call__(self, tank): + if random.randint(1,self._m) <= self._n: + return True + else: + return False + +class Sin(Function.Function): + """sin(T) + A sin wave of period T (in turns). Returns True when the wave is positive. + A wave with period 1 or 2 is always False (it's 0 each turn), only + at periods of 3 or more does this become useful.""" + + def __init__(self, T): + self._T = T + + def __call__(self, tank): + turn = tank.turn + factor = math.pi/self._T + if math.sin(turn * factor) > 0: + return True + else: + return False + +class Cos(Function.Function): + """cos(T) + A cos wave with period T (in turns). Returns True when the wave is + positive. A wave of period 1 is always True. Period 2 is True every + other turn, etc.""" + + def __init__(self, T): + self._T = T + + def __call__(self, tank): + + turn = tank.turn + factor = math.pi/self._T + if math.cos(turn * factor) > 0: + return True + else: + return False + +class FireReady(Function.Function): + """fireready() + True when the tank can fire.""" + def __call__(self, tank): + return tank.fireReady() + +class FireNotReady(Function.Function): + """firenotready() + True when the tank can not fire.""" + def __call__(self, tank): + return not tank.fireReady() + +### When adding names to this dict, make sure they are lower case and alpha +### numeric. +conditions = {'sense': Sense, + 'random': Random, + 'toggle': Toggle, + 'sin': Sin, + 'cos': Cos, + 'fireready': FireReady, + 'firenotready': FireNotReady, + 'timer': TimerCheck } diff --git a/tanks/docs.py b/tanks/docs.py new file mode 100644 index 0000000..0318250 --- /dev/null +++ b/tanks/docs.py @@ -0,0 +1,26 @@ +import xml.sax.saxutils + +def mkDocTable(objects): + objects.sort(lambda o1, o2: cmp(o1.__doc__, o2.__doc__)) + + for object in objects: + if object.__doc__ is None: + print '
%s
Bad object
' % \ + xml.sax.saxutils.escape(str(object)) + continue + text = object.__doc__ + lines = text.split('\n') + head = lines[0].strip() + head = xml.sax.saxutils.escape(head) + + body = [] + for line in lines[1:]: + line = line.strip() #xml.sax.saxutils.escape( line.strip() ) + line = line.replace('.', '.
') + body.append(line) + + body = '\n'.join(body) + print '
%s
%s
' % (head, body) + #print '%sIntentionally blank%s' % (head, body) + + diff --git a/tanks/setup.py b/tanks/setup.py new file mode 100644 index 0000000..81a402c --- /dev/null +++ b/tanks/setup.py @@ -0,0 +1,72 @@ +"""Each of these classes provides a function for configuring a tank. +They should inherit from Function.Function. +To make one available to the tank programmer, add it to the dictionary at +the end of this file.""" + +import Function + +class AddSensor(Function.Function): + """addsensor(range, angle, width, [turretAttached]) +Add a new sensor to the tank. Sensors are an arc (pie slice) centered on +the tank that detect other tanks within their sweep. +A sensor is 'on' if another tank is within this arc. +Sensors are numbered, starting at 0, in the order they are added. +

+range - The range of the sensor, as a percent of the tanks max range. +angle - The angle of the center of the sensor, in degrees. +width - The width of the sensor, in percent (100 is a full circle). +turretAttached - Normally, the angle is relative to the front of the +tank. When this is set, the angle is relative to the current turret +direction. +

+Sensors are drawn for each tank, but not in the way you might expect. +Instead of drawing a pie slice (the actual shap of the sensor), an arc with +the end points connected by a line is drawn. Sensors with 0 width don't show +up, but still work. +""" + + def __init__(self, range, angle, width, turretAttached=False): + + self._checkRange(range, 'sensor range') + + self._range = range / 100.0 + self._width = self._convertAngle(width, 'sensor width') + self._angle = self._convertAngle(angle, 'sensor angle') + self._turretAttached = turretAttached + + def __call__(self, tank): + tank.addSensor(self._range, self._angle, self._width, + self._turretAttached) + +class AddToggle(Function.Function): + """addtoggle([state]) +Add a toggle to the tank. The state of the toggle defaults to 0 (False). +These essentially act as a single bit of memory. +Use the toggle() condition to check its state and the settoggle, cleartoggle, +and toggle actions to change the state. Toggles are named numerically, +starting at 0. +""" + def __init__(self, state=0): + self._state = state + + def __call__(self, tank): + if len(tank.toggles) >= tank.SENSOR_LIMIT: + raise ValueError('You can not have more than 10 toggles.') + + tank.toggles.append(self._state) + +class AddTimer(Function.Function): + """addtimer(timeout) +Add a new timer (they're numbered in the order added, starting from 0), +with the given timeout. The timeout is in number of turns. The timer +is created in inactive mode. You'll need to do a starttimer() action +to reset and start the timer. When the timer expires, the timer() +condition will begin to return True.""" + def __init__(self, timeout): + self._timeout = timeout + def __call__(self, tank): + tank.addTimer(timeout) + +setup = {'addsensor': AddSensor, + 'addtoggle': AddToggle, + 'addtimer': AddTimer} diff --git a/template.html b/template.html new file mode 100644 index 0000000..40c908e --- /dev/null +++ b/template.html @@ -0,0 +1,26 @@ + + + + + $title + + $hdr + + +

$title

+ + $body + + diff --git a/www/ctf.css b/www/ctf.css new file mode 100644 index 0000000..0bacbf1 --- /dev/null +++ b/www/ctf.css @@ -0,0 +1,184 @@ +/**** document ****/ + +html { + background: #222 url(grunge.png) repeat-x; +} + +body { + font-family: sans-serif; + color: #fff; + margin: 50px 0 0 110px; + padding: 10px; + max-width: 700px; +} + +/**** heading ****/ + +h1:first-child { + text-transform: lowercase; + font-size: 1.6em; +/* background-color: #222; */ +/* opacity: 0.9; */ + padding: 3px; + color: #2a2; + margin: 0 0 1em 70px; +} + +h1:first-child:before { + color: #fff; + letter-spacing: -0.1em; + content: "Capture The Flag: "; +} + +/*** left side bar ***/ + +#navigation { + position: absolute; + background: #222; + opacity: 0.9; + top: 80px; + left: 0px; + padding: 0; +} + +#navigation h3 { + font-size: 100%; + border-bottom: 2px solid #444; +} + +#navigation ul { + list-style: none; + padding: 0; + margin: 0; +} + +#navigation li a { + display: block; + height: 25px; + width: 90px; + padding: 5px; + margin: 5px; + background: inherit; + border-right: 4px solid #444; + color: #999; + text-transform: lowercase; + font-size: 0.9em; +} + +#navigation li a:hover { + color: #f4f4f4; + background: #333; + border-right: 4px solid #2a2; +} + +#navigation li .active { + color: #999; + background: #333; + border-right: 4px solid #444; +} + + +/**** body ****/ + +a img { + border: 0px; +} + +a { + text-decoration: none; + color: #2a2; + font-weight: bold; +} + +a:hover { + color: #fff; + background: #2a2; + font-weight: bold; +} + + +h1, h2, h3 { + color: #999; + letter-spacing: -0.05em; +} + +.readme { + color: #fff; + background-color: #555; + margin: 1em; +} + +pre { + color: #fff; + background-color: #222; + border: solid #ccc 2px; + padding: 0.25em; +} + + +th, td { + vertical-align: top; +} + +p { + line-height: 1.4em; + margin-bottom: 20px; + color: #f4f4f4; +} + +hr { + border: 1px solid #444; +} + +dt { + white-space: pre; + background-color: #333; + padding: 5px; + border: 2px solid green; + border-bottom: none; + font-weight: bold; +} + +dd { + border: 2px solid green; + margin: 0px; + padding: 5px; + background-color: #282828; +} + + +/**** special cases ****/ + +.wide { + max-width: inherit; +} + +.scoreboard { + background: #222; +} + +.scoreboard td { + height: 400px; +} + +#battlefield { + border: 2px solid green; +} + +.solved { + text-decoration: line-through; +} + +table.pollster { + margin-left: 5em; +} + +table.pollster td { + padding: 2px 1em 2px 5px; +} + +table.pollster thead { + font-weight: bold; +} + + diff --git a/www/grunge.png b/www/grunge.png new file mode 100644 index 0000000..2b98730 Binary files /dev/null and b/www/grunge.png differ diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..463085c --- /dev/null +++ b/www/index.html @@ -0,0 +1,28 @@ + + + + + Capture The Flag + + + +

Welcome

+ +
    +
  1. Take the survey
  2. +
  3. Read the introduction and rules
  4. +
  5. Register your team
  6. +
  7. View the score board
  8. +
+ +
    +
  • Some categories are puzzles.
  • +
  • Some categories live on the network; you have to find them + yourself.
  • +
  • Announcements will be made + on IRC.
  • +
+ + + diff --git a/www/intro.html b/www/intro.html new file mode 100644 index 0000000..04cf503 --- /dev/null +++ b/www/intro.html @@ -0,0 +1,122 @@ + + + + + Introduction + + + +

Introduction

+ +

+ Welcome to Capture The Flag. +

+ +

What This Is

+ +
    +
  • A hacking contest
  • +
  • A chance to experience the nature of cyber incident response
  • +
  • An environment to safely experiment with offensive techniques
  • +
+ + +

What This Is Not

+ +
    +
  • An arena for purely malicious attacks
  • +
  • A rave
  • +
+ +

Rules

+ +

Important Rules

+
    +
  • The contest network is 10.x.x.x. Do + not attack machines outside the contest network. All + federal, state, and school laws still apply to the outside + network.
  • +
  • If the "outside network" requires you to plug into a different + switch, do not connect any machine that has been on the contest + network.
  • +
  • Consider this network hostile: your machine may be + compromised.
  • +
  • We expect you to be disruptive within the framework of the + game (malicious code, network scanning, social engineering, + etc.). Disruptive behavior outside the game will result in a + public and humiliating ejection from the contest area.
  • +
  • No ARP attacks. While cute, they are not particularly clever + given our network topology, and would require expensive and + bulky equipment to prevent. Find something else to do.
  • +
+ +

Less-Important Rules

+
    +
  • If IRC is up, you should use it to communicate with the + contest staff. Staff will have operator status in #ctf.
  • +
  • If you think something is wrong with the game, you are + expected to demonstrate the problem and explain what you think + is the correct behavior.
  • +
+ +

Scoring

+ +

+ The contest is made up of multiple categories. Each + category is worth one point toward the total score; the percentage + of the total points held by your team is the percentage of one + point your team has for that category. The team that has 30% of + the points in each of five categories has 1.5 points, whereas the + team that has 80% of the points in only one category has 0.8 + points. +

+ +

+ There are two kinds of categories: flags + and puzzles. +

+ +

Flags

+ +

+ Flag categories are challenges with a notion of a winner + or service availability. In these categories, the + flag-holder (the winner, or each team with a running service) + makes 1 point per minute for as long as they hold the flag. If + there is a single flag-holder, and the flag changes hands, a point + is awarded to the new winner at the moment the flag moves. +

+ +

Puzzles

+ +

+ Most of the categories come in the form of + multiple puzzles: for each puzzle presented, a key + (answer) must be found to recieve the amount of points that puzzle + is worth. Any team may answer any puzzle question at any time. A + new puzzle is revealed when a team correctly answers the + highest-valued puzzle in that category. +

+ +

Hints

+ +

+ If you are really stuck, you can ask for a hint. It will cost you + points, though. For puzzles, you will lose ΒΌ of the points for + that puzzle even if you never solve the puzzle. For + other events, the staff member will decide how many points it will + cost. You can try to bribe or otherwise fanagle information out + of us or other contestants. It's a hacking contest. +

+ +

About Us

+ +

+ We are the dirtbags. People + pay us money to do the sorts of things you'll be doing in this + contest. +

+ + + diff --git a/www/plot.js b/www/plot.js new file mode 100644 index 0000000..e62ec60 --- /dev/null +++ b/www/plot.js @@ -0,0 +1,42 @@ +function Plot(id, width, height) { + var canvas = document.getElementById(id); + var ctx = canvas.getContext('2d'); + + canvas.width = 800; + canvas.height = 200; + + // We'll let the canvas do all the tricksy math + xscale = canvas.width/width; + yscale = canvas.height/height; + ctx.lineWidth = 2; + + function moveTo(x, y) { + ctx.moveTo(Math.round(x * xscale), Math.round(y * yscale)); + } + function lineTo(x, y) { + ctx.lineTo(Math.round(x * xscale), Math.round(y * yscale)); + } + + function draw(values) { + ctx.beginPath(); + moveTo(values[0][0], height); + var lasty = 0; + for (i in values) { + var x = values[i][0]; + var y = values[i][1]; + lineTo(x, height - lasty); + lineTo(x, height - y); + lasty = y; + } + lineTo(width, height - lasty); + } + + + this.line = function(color, values) { + ctx.fillStyle = color; + ctx.strokeStyle = color; + + draw(values); + ctx.stroke(); + } +} diff --git a/www/puzzler.cgi b/www/puzzler.cgi new file mode 100755 index 0000000..f076258 --- /dev/null +++ b/www/puzzler.cgi @@ -0,0 +1,186 @@ +#! /usr/bin/python + +## +## This is pretty crufty :< +## + +import cgitb; cgitb.enable() +import cgi +import os +import fcntl +import re +import sys +from cgi import escape +import Cookie as cookies +from urllib import quote, unquote +from codecs import open +from sets import Set as set +from cStringIO import StringIO + +from ctf import pointscli, teams, html, paths + +keysfile = os.path.join(paths.LIB, 'puzzler.keys') +datafile = os.path.join(paths.VAR, 'puzzler.dat') +puzzles_dir = os.path.join(paths.WWW, 'puzzler') + +## +## This allows you to edit the URL and work on puzzles that haven't been +## unlocked yet. For now I think that's an okay vulnerability. It's a +## hacking contest, after all. +## + +cat_re = re.compile(r'^[a-z]+$') +points_re = re.compile(r'^[0-9]+$') + +points_by_cat = {} +points_by_team = {} +try: + for line in open(datafile, encoding='utf-8'): + cat, team, pts = [unquote(v) for v in line.strip().split('\t')] + pts = int(pts) + points_by_cat[cat] = max(points_by_cat.get(cat, 0), pts) + points_by_team.setdefault((team, cat), set()).add(pts) +except IOError: + pass + + +c = cookies.SimpleCookie(os.environ.get('HTTP_COOKIE', '')) +try: + team = c['team'].value + passwd = c['passwd'].value +except KeyError: + team, passwd = None, None + +f = cgi.FieldStorage() +cat = f.getfirst('c') +points = f.getfirst('p') +team = f.getfirst('t', team) +passwd = f.getfirst('w', passwd) +key = f.getfirst('k', '').decode('utf-8') + +def serve(title, sf=None, **kwargs): + if team or passwd: + c = cookies.SimpleCookie() + if team: + c['team'] = team + if passwd: + c['passwd'] = passwd + print(c) + if not sf: + sf = StringIO() + return html.serve(title, sf.getvalue(), **kwargs) + +def safe_join(*args): + safe = list(args[:1]) + for a in args[1:]: + if not a: + return None + else: + a = a.replace('..', '') + a = a.replace('/', '') + safe.append(a) + ret = '/'.join(safe) + if os.path.exists(ret): + return ret + +def dump_file(fn): + f = open(fn, 'rb') + while True: + d = f.read(4096) + if not d: + break + sys.stdout.buffer.write(d) + +def disabled(cat): + return os.path.exists(os.path.join(paths.LIB, 'disabled', cat)) + +def show_cats(): + out = StringIO() + out.write('
    ') + puzzles = os.listdir(puzzles_dir) + puzzles.sort() + for p in puzzles: + if disabled(p): + continue + out.write('
  • %s
  • ' % (html.base, p, p)) + out.write('
') + serve('Categories', out) + + +def show_puzzles(cat, cat_dir): + out = StringIO() + opened = points_by_cat.get(cat, 0) + puzzles = ([int(v) for v in os.listdir(cat_dir)]) + puzzles.sort() + if puzzles: + out.write('
    ') + for p in puzzles: + cls = '' + try: + if p in points_by_team[(team, cat)]: + cls = 'solved' + except KeyError: + pass + out.write('
  • %(points)d
  • ' % + {'base': html.base, + 'cat': cat, + 'points': p, + 'class': cls}) + if p > opened: + break + out.write('
') + else: + out.write('

None (someone is slacking)

') + serve('Open in %s' % escape(cat), out) + +def win(cat, team, points): + out = StringIO() + points = int(points) + f = open(datafile, 'a', encoding='utf-8') + pointscli.award(cat, team, points) + fcntl.lockf(f, fcntl.LOCK_EX) + f.write('%s\t%s\t%d\n' % (quote(cat), quote(team), points)) + out.write('

%d points for %s.

' % (points, cgi.escape(team))) + out.write('

Back to %s.

' % (html.base, cat, cat)) + serve('Winner!', out) + +def check_key(cat, points, candidate): + for line in open(keysfile, encoding='utf-8'): + thiscat, thispoints, key = line.split('\t', 2) + if (cat, points) == (thiscat, thispoints): + if key.rstrip() == candidate: + return True + return False + +def main(): + cat_dir = safe_join(puzzles_dir, cat) + points_dir = safe_join(puzzles_dir, cat, points) + + if not cat_dir: + # Show categories + show_cats() + elif not points_dir: + # Show available puzzles in category + show_puzzles(cat, cat_dir) + else: + if not teams.chkpasswd(team, passwd): + serve('Wrong password') + elif not check_key(cat, points, key): + serve('Wrong key') + elif int(points) in points_by_team.get((team, cat), set()): + serve('Greedy greedy') + else: + win(cat, team, points) + +if __name__ == '__main__': + import optparse + import sys, codecs + + sys.stdout = codecs.getwriter('utf-8')(sys.stdout) + + main() + + +# Local Variables: +# mode: python +# End: diff --git a/www/register.cgi b/www/register.cgi new file mode 100755 index 0000000..6318ae9 --- /dev/null +++ b/www/register.cgi @@ -0,0 +1,63 @@ +#! /usr/bin/python + +import cgitb; cgitb.enable() +import cgi +import os +import fcntl +import string + +from ctf import teams, html + +def main(): + f = cgi.FieldStorage() + + team = f.getfirst('team', '') + pw = f.getfirst('pw') + confirm_pw = f.getfirst('confirm_pw') + + tmpl = string.Template(''' +

+ Pick a short team name: you'll be typing it a lot. +

+ +
+
+ Registration information: + + + + $team_error
+ + + +
+ + + + $pw_match_error
+ + +
+
''') + + if not (team and pw and confirm_pw): # If we're starting from the beginning? + body = tmpl.substitute(team_error='', + pw_match_error='') + elif teams.exists(team): + body = tmpl.substitute(team_error='Team team already taken', + pw_match_error='') + elif pw != confirm_pw: + body = tmpl.substitute(team_error='', + pw_match_error='Passwords do not match') + else: + teams.add(team, pw) + body = ('

Congratulations, %s is now registered. Go back to the front page and start playing!

' % cgi.escape(team)) + + html.serve('Team Registration', body) + +if __name__ == '__main__': + import sys, codecs + + sys.stdout = codecs.getwriter('utf-8')(sys.stdout) + + main() diff --git a/www/services.html b/www/services.html new file mode 120000 index 0000000..f149dd9 --- /dev/null +++ b/www/services.html @@ -0,0 +1 @@ +/tmp/services.html \ No newline at end of file diff --git a/www/survey.cgi b/www/survey.cgi new file mode 100755 index 0000000..bb88b4d --- /dev/null +++ b/www/survey.cgi @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +import cgi +import cgitb +import os +import time + +cgitb.enable() + +form = cgi.FieldStorage() +client = os.environ["REMOTE_ADDR"] + +fields = { + 'affiliation' : ['nnsa', 'doe', 'dod', 'otherfed', 'state', 'private', 'other'], + 'hostforensics' : ['has', 'doesnt_have_can_get', 'doesnt_have_cant_get'], + 'netforensics' : ['has', 'doesnt_have_can_get', 'doesnt_have_cant_get'], + 'reversing' : ['has', 'doesnt_have_can_get', 'doesnt_have_cant_get'], + 'regularcollab' : ['0', '1', '2', '3', '4', '5+'], + 'collab' : ['0', '1', '2', '3', '4', '5+'], + 'incident' : ['0', '1', '2', '3', '4', '5+'], + 'channels' : ['official', 'unofficial'], + 'helpfulone' : ['tracer', 'cons', 'vtc', 'tc', 'irc'], + 'helpfultwo' : ['tracer', 'cons', 'vtc', 'tc', 'irc'], + 'helpfulthree' : ['tracer', 'cons', 'vtc', 'tc', 'irc'], + 'helpfulfour' : ['tracer', 'cons', 'vtc', 'tc', 'irc'], + 'helpfulfive' : ['tracer', 'cons', 'vtc', 'tc', 'irc'], + 'toolset' : ['0', '1', '2', '3', '4'], + 'overall' : ['0', '1', '2', '3', '4'], + 'comments' : [] + } + +def validate(form): + + for k,v in fields.items(): + if len(v) and form.getfirst(k) not in v: + return False + + vals = [] + for k in ['helpfulone', 'helpfultwo', 'helpfulthree', 'helpfulfour', 'helpfulfive']: + if form.getfirst(k) in vals: + return False + vals.append(form.getfirst(k)) + + return True + +print 'Content-Type: text/html' +print '' + +print ''' + + + + CyberTracer Collaboration Survey + + + + +
+ + + +
+''' + +if validate(form): + results = [client, str(time.time())] + + for k in fields.keys(): + val = form.getfirst(k) or '' + if k == 'comments': + val = val.replace(',', ' ') + val = val.replace(':', ' ') + val = val.replace('\n', ' ') + val = val.replace('\r', ' ') + results.append('%s:%s' % (k, val)) + + f = open('/var/lib/ctf/survey/%s' % client, 'a') + f.write(','.join(results) + '\n') + f.close() + + print '

SUCCESS! Your survey submission has been accepted. Please do not retake the survey. Thanks!

' +else: + print ''' +

FAIL! It looks like you bypassed the client-side validation of the survey! That's too easy and the contest + hasn't even begun yet! Would you please go back and just take the survey? It is very important!

+ ''' + +print ''' +
+
+ + +''' diff --git a/www/survey.css b/www/survey.css new file mode 100644 index 0000000..f2b8413 --- /dev/null +++ b/www/survey.css @@ -0,0 +1,125 @@ +html { + background: #454545; +} + +body { + margin: 0; + padding: 0; + border: 0; + border-right: 1px solid #000; + border-bottom: 1px solid #000; + width: 100%; + height: 100%; + background: #fff; + max-width: 800px; + color: #000; + font-size: 0.7em; + font-family: Tahoma, Arial, sans-serif; +} + +#wrapper { + padding: 5em; +} + +#header { + border-bottom: 1px solid #373737; +} + +#content { + padding: 1em 0; +} + +p { + margin: 0; + padding: 2px 0 2px 0; +} + +a { color: #369; } +a:hover { + color: #fff; + background: #369; + text-decoration: none; +} + +img { + padding: 0; + margin: 0; + border: none; + vertical-align: top; +} + +table { + margin: 0.5em; + padding: 0; + border: 1px solid #373737; + border-collapse: collapse; +} +thead { + font-weight: bold; + background: #fff; + border-bottom-style: double; +} +tr { + padding: 0; + margin: 0; + border: 1px solid #373737; +} +td { + margin: 0; + padding: 2px 5px 2px 5px; + border: 1px dotted #c0c0c0; +} + +h1, h2, h3, h4 { margin: .5em 0 .2em 0; } +h1 { font-size: 150%; } +h2 { font-size: 120%; } +h3 { font-size: 110%; } +h4 { font-size: 100%; } + +fieldset { margin-top: 1em; } +input { + font-size: 100%; + margin: .25em .25em 0 .5em; +} + +textarea { + width: 97%; + margin: .25em .25em 0 .5em; +} + +.sep { + height:1px; + margin: 1em 0 1em 0; + border-bottom: 1px dashed #c0c0c0; +} + +.question { + font-weight: bold; + margin-bottom: .5em; +} + +ul { + margin: .25em .25em 0 .5em; + padding: 0 1.25em; +} + +li { + margin-bottom: 1em; +} + +.submit { + text-align: right; + margin-top: 1em; + border-top: 1px solid #373737; + padding-top: 1em; +} + +.error { + margin: 1em 0; + padding: 1em; + border: 1px solid red; + background: #ffaaaa; + color: #000; + font-weight: bold; +} + diff --git a/www/survey.html b/www/survey.html new file mode 100644 index 0000000..99da9cb --- /dev/null +++ b/www/survey.html @@ -0,0 +1,344 @@ + + + + CyberTracer Collaboration Survey + + + + + + + +
+ + + +
+ +

Please take a few minutes to fill out and submit this survey. One of + the primary goals of the Cyber Tracer Team, the people behind Tracer FIRE, + is to improve the existing collaboration environment between NNSA sites + and other stakeholders. The information you provide in this survey will help + us to accomplish that goal.

+ +
+ +
+ +
+ +

1. What is your affiliation?

+ NNSA
+ DOE
+ DOD
+ Other Federal government
+ State government
+ Private sector
+ Other + +
+ +

2. For each skill area listed, select the option that best describes the situation + at your site or within your organization.

+
    +
  • Host forensics
    + have an advanced capability in this area
    + don't have an advanced capability in this area, but + can obtain help from other sites
    + don't have an advanced + capability in this area, and cannot + obtain help from other sites +
  • +
  • Network forensics
    + have an advanced capability in this area
    + don't have an advanced capability in this area, but + can obtain help from other sites
    + don't have an advanced + capability in this area, and cannot + obtain help from other sites +
  • +
  • Reverse engineering
    + have an advanced capability in this area
    + don't have an advanced capability in this area, but + can obtain help from other sites
    + don't have an advanced + capability in this area, and cannot + obtain help from other sites +
  • +
+ +
+ +

3. How many other sites or organizations do you + regularly collaborate with? "Regular" collaboration + is defined as collaboration that is frequent or part of standard operation procedure.

+ 0 + 1 + 2 + 3 + 4 + 5+ + +
+ +

4. How many other sites or organizations did you collaborate + with in all of FY09?

+ 0 + 1 + 2 + 3 + 4 + 5+ + +
+ +

5. If there were a serious cyber security incident at your site, + how many organizations would you feel comfortable calling for help?

+ 0 + 1 + 2 + 3 + 4 + 5+ + +
+ +

6. Would you prefer to use official channels (e.g., make a request to a team dedicated to + vetting and fielding incident responders from around the complex) or unofficial channels (e.g., pick up the + phone and call the reverse engineering expert you exchanged business cards with at Tracer FIRE) to request + help with an incident?

+ Official channels + Unofficial channels + +
+ +

7. Rank the items listed below from least to most helpful (1 = least helpful, + 5 = most helpful) for establishing trust relationships and fostering collaboration between sites.

+ + + + + + + + + + + + + + + + + + + + + +
Tracer FIRE + 1 + 2 + 3 + 4 + 5 +
Cyber security conferences + 1 + 2 + 3 + 4 + 5 +
Video teleconferencing + 1 + 2 + 3 + 4 + 5 +
Teleconferencing + 1 + 2 + 3 + 4 + 5 +
SILC or IRC + 1 + 2 + 3 + 4 + 5 +
+ +
+ +

8. How effective is the toolset (hardware and software) that is available to you + for cyber collaboration?

+ Not effective at all + Somewhat effective + Sufficient + Very effective + Couldn't be better + +
+ +

9. How effective is the existing collaboration environment between NNSA sites + and other stakeholders?

+ Not effective at all + Somewhat effective + Sufficient + Very effective + Couldn't be better + +
+

10. Do you have any suggestions or ideas for improving the cyber collaboration + environment between NNSA sites and other stakeholders? Please be brief.

+ + +
+ +
+ +
+
+
+ + + diff --git a/www/tanks/docs.html b/www/tanks/docs.html new file mode 100644 index 0000000..e4d722d --- /dev/null +++ b/www/tanks/docs.html @@ -0,0 +1,360 @@ + + + + + Tanks Documentation + + + +

Tanks Documentation

+ + + +

Introduction

+ +

+ You are the proud new operator of a M-375 Pflanzarr Tank. Your + tank is equipped with a powerful laser cannon, independently + rotating turret section, up to 10 enemy detection sensors, and a + standard issue NATO hull. Unfortunately, it lacks seats, and thus + must rely own its own wits and your skills at designing those wits + to survive. +

+ +

Programming Your Tank

+ +

+ Your tanks are programmed using the Super Useful Command and + Kontrol language, the very best in laser tank AI languages. It + includes amazing features such as comments (Started by a #, ended + at EOL), logic, versatility, and semi-colons (all lines must end + in one). As with all new military systems it utilizes only + integers; we must never rest in our diligence against the + communist floating point conspiracy. Whitespace is provided by + trusted contractors, and should never interfere with operations. +

+ +

+ Your program should be separated into Setup and AI commands. The + definitions section lets you designated the behaviors of its + sensors and memory. Each setup command must begin with a + '>'. Placing setup commands after the first AI command is a + violation of protocol. Here are some examples of correct setup + commands: +

+ +
>addsensor(80, 90, 33);
+>addsensor(50, 0, 10, 1);
+>addtimer(3);
+ +

+ The AI section will act as the brain of your tank. Each AI line + is separated into a group of conditions functions and a group of + action functions. If all the conditions are satisfactory (true), + all of the actions are given as orders. Conditions are separated + by ampersands, actions separated by periods. Here are some + examples of AI commands: +

+ +
sense(1) & sense(2) & fireready() : fire();
+sense(0,0)&sin(5): move(40, 30) . turretcw(50);
+sense(4) & random(4,5) :
+led(1).settoggle(0,1);
+ +

+ Your tank will execute its program each turn(frame), and attempt + to the best of its abilities to carry out its orders (or die + trying). Like any military mind, your tank may receive a plethora + of often conflicting orders and information. This a SMART TANK, + however. It knows that the proper thing to do with each subsystem + is to have that subsystem follow only the last order given each + turn. +

+ +
#Setup commands define your tank when your program
+#compiles
+>addsensor(50, 0, 5, 1); # 0-Fire Sensor
+>addsensor(30, 0, 180); # 1-Anti-collision sensor
+
+# These commands execute each frame.
+# Blank condition sections are true.
+         : move(90, 100).
+           turretset(0);
+sense(0) : fire();
+sense(1) : move(-100, 100)
+ +

Setup Actions:

+ +

+ These functions can be used to setup your tank. Abuse of these + functions has, in the past, resulted in mine sweeping duty. With + a broom. +

+ +

+

+
addsensor(range, angle, width, [turretAttached])
+
+

Add a new sensor to the tank.

+

+ Sensors are an arc (pie slice) centered on the tank that + detect other tanks within their sweep.
+ A sensor is 'on' if another tank is within this arc. +

+

+ Sensors are numbered, starting at 0, in the order they are + added. +

+

+ range - The range of the sensor, as a percent of the tanks max + range.
+ angle - The angle of the center of the sensor, in degrees.
+ width - The width of the sensor, in degrees.
+ turretAttached - Normally, the angle is relative to the front of + the + tank.
When this is set, the angle is relative to the current + turret + direction.
+

+
+ +
addtimer(timeout)
+
+

+ Add a new timer (they're numbered in the order added, starting from 0), + with the given timeout. +

+

+ The timeout is in number of turns.
+ The timer + is created in inactive mode.
You'll need to do a starttimer() + action + to reset and start the timer.
When the timer expires, the + timer() + condition will begin to return True. +

+
+ +
addtoggle([state])
+
+

Add a toggle to the tank.

+

+ The state of the toggle defaults to 0 (False).
+ These essentially act as a single bit of memory.
+ Use the toggle() condition to check its state and the settoggle, + cleartoggle, + and toggle actions to change the state.
Toggles are named + numerically, + starting at 0. +

+
+
+ +

Conditions:

+ +

+ These functions are used to check the state of reality. If reality + stops being real, refer to chapter 5 in your girl scout + handbook. +

+ +
+
cos(T)
+
+

+ A cos wave with period T (in turns). +

+ +

+ Returns True when the wave is + positive.
A wave of period 1 is always True.
Period + 2 is True every + other turn, etc. +

+
+ +
firenotready()
+
+

+ True when the tank can not fire. +

+
+ +
fireready()
+
+

+ True when the tank can fire. +

+
+ +
random(n,m)
+
+

Generate a random number.

+ +

+ Takes two + arguments, n and m.
Generates a random number between 1 + and m (inclusive) each time it's checked.
If the random + number is less + than or equal + to n, then the condition returns True.
Returns False + otherwise. +

+
+ +
sense(#, [invert])
+
+

True when a sensor is activated.

+ +

+ Takes a Sensor number as an argument.
+ + Returns True if the given sensor is currently activated, False + otherwise.
+ If the option argument invert is set to true then logic is + inverted, + and then sensor returns True when it is NOT activated, and + False when + it is.
Invert is false by default. +

+
+ +
sin(T)
+
+

A sin wave of period T (in turns).

+ +

+ Returns True when the wave is positive.
+ A wave with period 1 or 2 is always False (it's 0 each turn), + only + at periods of 3 or more does this become useful. +

+
+ +
timer(#, [invert])
+ +
+

Checks the state of timer # 'key'.

+ +

+ Returns True if time has run + out.
+ + If invert is given (and true), then True is returned if the + timer has + yet to expire. +

+
+ +
toggle(#)
+
+

Returns True if the given toggle is set, False + otherwise.

+
+
+ +

Actions:

+ +

+ These actions are not for cowards. Remember, if actions + contradict, your tank will simply do the last thing it was told in + a turn. If ordered to hop on a plane to hell it will gladly do + so. If order to make tea shortly afterwards, it will serve it + politely and with cookies instead. +

+ + +
+
cleartimer(#)
+
+

Clear the given timer such that it is no longer active (inactive timers + are always False).

+
+ +
fire()
+
+

Attempt to fire the tanks laser cannon.

+

+ Its range is 50% of your sensor range. +

+
+ +
led(state)
+
+

Set the tank's LED to state (true is on, false is off).

+

+ The led is a light that appears behind the tanks turret.
+ It remains on for a single turn. +

+
+ +
move(left tread speed, right tread speed)
+
+

Set the speeds for the tanks right and left treads.

+ +

+ The speeds should + be a number (percent power) between -100 and + 100. +

+
+ +
settoggle(key, state)
+
+

Set toggle 'key' to 'state'.

+
+ +
starttimer(#)
+
+

Start (and reset) the given timer, but only if it is + inactive.

+
+ +
toggle(key)
+
+

Toggle the value of toggle 'key'.

+
+ +
turretccw([percent speed])
+
+

Rotate the turret counter clockwise as a + percentage of the max speed.

+
+ +
turretcw([percent speed])
+
+

Rotate the turret clockwise at a rate + preportional to speed.

+
+ +
turretset([angle])
+
+

Set the turret to the given angle, in degrees, relative to the + front of the tank.

+

+ Angles increase counterclockwise.
The angle can be left + out; in that case, this locks the turret to its current + position. +

+
+
+ + diff --git a/www/tanks/errors.cgi b/www/tanks/errors.cgi new file mode 100755 index 0000000..32ece0f --- /dev/null +++ b/www/tanks/errors.cgi @@ -0,0 +1,54 @@ +#!/usr/bin/python + +import cgi +import cgitb; cgitb.enable() +import sys +import os + +from urllib import quote + +from ctf import teams, html + +basedir = '/var/lib/ctf/tanks' + +links = ''' +

Tanks

+
  • Docs
  • +
  • Results
  • +
  • Submit
  • +
  • My Errors
  • +''' + +body = [] +fields = cgi.FieldStorage() +team = fields.getfirst('team', '').strip() +passwd = fields.getfirst('passwd', '').strip() +if teams.chkpasswd(team, passwd): + path = os.path.join(basedir, 'errors', quote(team)) + if os.path.isfile(path): + body.append('

    Your latest errors:

    ') + errors = open(path).readlines() + if errors: + body.append('
      ') + for e in errors: + body.append('
    • %s
    • ' % cgi.escape(e)) + body.append('
    ') + else: + body.append('

    There were no errors.

    ') + else: + body.append('

    No error file found.

    ') +else: + body.append('Authentication failed.') + +body.append(''' +
    +
    + Error report request: + Team:
    + Password:
    + +
    +
    ''') + +html.serve('Tanks Errors', '\n'.join(body), links=links) + diff --git a/www/tanks/results.cgi b/www/tanks/results.cgi new file mode 100755 index 0000000..e6632f1 --- /dev/null +++ b/www/tanks/results.cgi @@ -0,0 +1,33 @@ +#! /usr/bin/python + +import os +from ctf import html, paths +from cgi import escape + +basedir = os.path.join(paths.VAR, 'tanks') + +links = ''' +

    Tanks

    +
  • Docs
  • +
  • Results
  • +
  • Submit
  • +
  • My Errors
  • +''' + +body = [] + +body.append('

    Last Winner:

    ') +body.append('

    ') +body.append(escape(open(os.path.join(basedir, 'winner')).read())) +body.append('

    ') +body.append('

    Results so far:

    ') +body.append('
      ') +results = os.listdir(os.path.join(basedir, 'results')) +results.sort() +results.reverse() +for fn in results: + num, _ = os.path.splitext(fn) + body.append('
    • %s
    • ' % (fn, num)) +body.append('
    ') + +html.serve('Tanks Results', '\n'.join(body), links=links) diff --git a/www/tanks/submit.cgi b/www/tanks/submit.cgi new file mode 100755 index 0000000..8750d3b --- /dev/null +++ b/www/tanks/submit.cgi @@ -0,0 +1,39 @@ +#!/usr/bin/python + +import cgi +import cgitb; cgitb.enable() +import os +import sys + +from urllib import quote + +from ctf import teams, html, paths + +basedir = os.path.join(paths.VAR, 'tanks') + +links = ''' +

    Tanks

    +
  • Docs
  • +
  • Results
  • +
  • Submit
  • +
  • My Errors
  • +''' + + +fields = cgi.FieldStorage() +team = fields.getfirst('team', '').strip() +passwd = fields.getfirst('passwd', '').strip() +code = fields.getfirst('code', '') +if not teams.chkpasswd(team, passwd): + body = '

    Authentication failed.

    ' +elif not code: + body = '

    No program given.

    ' +else: + path = os.path.join(basedir, 'ai/players', quote(team, safe='')) + file = open(path, 'w') + file.write(code) + file.close() + + body = ("

    Submission successful.

    ") + +html.serve('Tanks Submission', body, links=links) diff --git a/www/tanks/submit.html b/www/tanks/submit.html new file mode 100644 index 0000000..1edc859 --- /dev/null +++ b/www/tanks/submit.html @@ -0,0 +1,46 @@ + + + + + Tanks Submission + + + +

    Tanks Submission

    + + + +
    +
    + Your program: + Team: +
    + Password: +
    +
    + + +
    +
    + + + diff --git a/www/tanks/tanks.js b/www/tanks/tanks.js new file mode 100644 index 0000000..9e98d89 --- /dev/null +++ b/www/tanks/tanks.js @@ -0,0 +1,130 @@ +function torgba(color, alpha) { + var r = parseInt(color.substring(1,3), 16); + var g = parseInt(color.substring(3,5), 16); + var b = parseInt(color.substring(5,7), 16); + + return "rgba(" + r + "," + g + "," + b + "," + alpha + ")"; +} + +function start(turns) { + var canvas = document.getElementById('battlefield'); + var ctx = canvas.getContext('2d'); + var loop_id; + + + function crater(color, x, y, rotation) { + var points = 7; + var angle = Math.PI / points; + + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation); + ctx.lineWidth = 2; + ctx.strokeStyle = torgba(color, 0.5); + ctx.fillStyle = torgba(color, 0.2); + ctx.beginPath(); + ctx.moveTo(12, 0); + for (i = 0; i < points; i += 1) { + ctx.rotate(angle); + ctx.lineTo(6, 0); + ctx.rotate(angle); + ctx.lineTo(12, 0); + } + ctx.closePath() + ctx.stroke(); + ctx.fill(); + ctx.restore(); + } + + function sensors(color, x, y, rotation, sensors) { + var sensor_color = torgba(color, 0.4); + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotation); + ctx.lineWidth = 1; + for (i in sensors) { + s = sensors[i]; + if (s[3]) { + ctx.strokeStyle = "#000"; + } else { + ctx.strokeStyle = sensor_color; + } + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.arc(0, 0, s[0], s[1], s[2], false); + ctx.closePath(); + ctx.stroke(); + } + ctx.restore(); + } + + function tank(color, x, y, rotation, turret, led, fire) { + ctx.save(); + ctx.fillStyle = color; + ctx.translate(x, y); + ctx.rotate(rotation); + ctx.fillRect(-5, -4, 10, 8); + ctx.fillStyle = "#777777"; + ctx.fillRect(-7, -9, 15, 5); + ctx.fillRect(-7, 4, 15, 5); + ctx.rotate(turret); + if (fire) { + ctx.fillStyle = color; + ctx.fillRect(0, -1, fire, 2); + } else { + if (led) { + ctx.fillStyle = "#ff0000"; + } else { + ctx.fillStyle = "#000000"; + } + ctx.fillRect(0, -1, 10, 2); + } + ctx.restore(); + } + + var frame = 0; + var lastframe = 0; + var fps = document.getElementById('fps'); + function update_fps() { + fps.innerHTML = (frame - lastframe); + lastframe = frame; + } + function update() { + var idx = frame % (turns.length + 20); + + frame += 1; + if (idx >= turns.length) { + return; + } + + canvas.width = canvas.width; + turn = turns[idx]; + + // Draw craters first + for (i in turn) { + t = turn[i]; + if (t[0]) { + crater(t[1], t[2], t[3], t[4]); + } + } + // Then sensors + for (i in turn) { + t = turn[i]; + if (! t[0]) { + sensors(t[1], t[2], t[3], t[4], t[8]); + } + } + // Then tanks + for (i in turn) { + t = turn[i]; + if (! t[0]) { + // Surely there's a better way. CBA right now. + tank(t[1], t[2], t[3], t[4], t[5], t[6], t[7]); + } + } + } + + loop_id = setInterval(update, 66); + setInterval(update_fps, 1000); +} +