Cranberry Pi Hacks

Cranberry Pi

Cranberry Pi terminals are running as Docker containers and are implemented as terminals over https using wetty.
More technically, under the hood, they are using either Ajax long-polling or WebSocket for connectivity and data.

Therefore some functionality is limited, e.g., screen recording, file transfer, etc. This must be fixed ;)

terminal access

As endpoint for wetty is simple socket.io, any client can connect to it. So the Cranberry Pi terminal client was made in Python - cpi.py. It's a bit hackish, but does the job. Screen can be recorded to .cast and rendered to .svg files via termtosvg. Files can be "transferred" both ways by using base64-encoding. To download a file, start a script or tee the terminal output and just base64 encode the necessary file; afterwards decode it. To upload a file, just copy/paste a base64-encoded string and decode it. Session is done, when ^C is pressed.

#!/usr/bin/env python
from __future__ import print_function, unicode_literals
import fcntl, shutil, select, sys, os, re, socketio, termios, time, tty, urllib3

urllib3.disable_warnings()
sio = socketio.Client()

@sio.on('connect')
def on_connect():
	ts = shutil.get_terminal_size((80, 24))
	sio.emit('resize', {"col":ts.columns, "row":ts.lines})

@sio.on('output', namespace='/')
def on_output(data):
        sys.stdout.write(data)
        sys.stdout.flush()

def console():
	fd = sys.stdin.fileno()
	fdtc = termios.tcgetattr(fd)
	fdfl = fcntl.fcntl(fd, fcntl.F_GETFL)
	fcntl.fcntl(fd, fcntl.F_SETFL, fdfl & ~os.O_NONBLOCK)
	tty.setraw(sys.stdin)
	pc = None
	while True:
		c = pc or ord(sys.stdin.read(1))
		pc = None
		if c == 3:
			sio.disconnect()
			break
		elif c == 27:
			fcntl.fcntl(fd, fcntl.F_SETFL, fdfl | os.O_NONBLOCK)
			r = chr(c) 
			while True:
				try:
					sc = sys.stdin.read(1)
					if not sc:
						break
					if sc == '\x1b':
						pc = ord(sc)
						break
					else:
						r += sc
				except IOError:
					break
				time.sleep(.001)
			fcntl.fcntl(fd, fcntl.F_SETFL, fdfl & ~os.O_NONBLOCK)
			if r:
				if ord(r[0]) == 3:
					sio.disconnect()
					break
				sio.emit('input', r)
			continue
		sio.emit('input', chr(c))
	fcntl.fcntl(fd, fcntl.F_SETFL, fdfl)
	termios.tcsetattr(fd, termios.TCSADRAIN, fdtc)

if __name__ == '__main__':
	if len(sys.argv) != 2:
		print('usage: {0} <challenge>'.format(sys.argv[0]))
		sys.exit(1)
	url = 'https://docker.kringlecon.com/wetty/socket.io/?challenge={0}'.format(sys.argv[1])
	sio.connect(url, socketio_path='wetty/socket.io', transports='websocket')
	sio.start_background_task(console)
	sio.wait()

challenge completion

It's cunning how the completion is being detected, when solving Cranberry Pi challenges. Basically all output from terminal is being filtered by a regex:

/#{5}hhc:(.*)#{5}/mi
Example message that is being captured (and not shown to terminal):
#####hhc:{"resourceId": "01234567-89ab-cdef-0123-456789abcdef", "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}#####
Afterwards, the hash, which is hmac256 of secret key and resourceId is submitted for verification to backend.

reverse engineering

Challenges, which have binaries like runtoanswer can be revere-engineered to, firstly, retrieve source code, secondly, to see accepted solutions and, finally, to retrieve secret key used for challenge completion. All of the challenge binaries (except The Sleighbell challenge) are written in Python and compiled to binary with PyInstaller. Reversing these files is pretty straight-forward.

First, extract the main function. Let's take Dev Ops Fail challenge as an example.

file runtoanswer
runtoanswer: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=28ba79c778f7402713aec6af319ee0fbaf3a8014, stripped
Run pyi-archive_viewer from PyInstaller to binary:
pyi-archive_viewer runtoanswer
 pos, length, uncompressed, iscompressed, type, name
[(0, 163, 180, 1, 'm', 'pyimod00_crypto_key'),
 (163, 254, 332, 1, 'm', 'struct'),
 (417, 1144, 1992, 1, 'm', 'pyimod01_os_path'),
 (1561, 4460, 10063, 1, 'm', 'pyimod02_archive'),
 (6021, 7539, 19710, 1, 'm', 'pyimod03_importers'),
 (13560, 1900, 4513, 1, 's', 'pyiboot01_bootstrap'),
 (15460, 1504, 2451, 1, 's', 'gitpasshist'),
 (16964,
  20293,
  40456,
  1,
  'b',
  'Crypto/Cipher/_AES.cpython-35m-x86_64-linux-gnu.so'),
 (37257, 8154, 22000, 1, 'b', '_bz2.cpython-35m-x86_64-linux-gnu.so'),
 (45411, 102648, 149880, 1, 'b', '_codecs_cn.cpython-35m-x86_64-linux-gnu.so'),
 (148059, 36047, 158104, 1, 'b', '_codecs_hk.cpython-35m-x86_64-linux-gnu.so'),
 (184106,
  9762,
  27032,
  1,
  'b',
  '_codecs_iso2022.cpython-35m-x86_64-linux-gnu.so'),
 (193868, 96674, 268664, 1, 'b', '_codecs_jp.cpython-35m-x86_64-linux-gnu.so'),
 (290542, 79128, 137592, 1, 'b', '_codecs_kr.cpython-35m-x86_64-linux-gnu.so'),
 (369670, 64003, 113016, 1, 'b', '_codecs_tw.cpython-35m-x86_64-linux-gnu.so'),
 (433673, 61562, 144336, 1, 'b', '_ctypes.cpython-35m-x86_64-linux-gnu.so'),
 (495235, 9597, 25360, 1, 'b', '_hashlib.cpython-35m-x86_64-linux-gnu.so'),
 (504832, 28114, 58504, 1, 'b', '_json.cpython-35m-x86_64-linux-gnu.so'),
 (532946, 14498, 37616, 1, 'b', '_lzma.cpython-35m-x86_64-linux-gnu.so'),
 (547444,
  20056,
  48240,
  1,
  'b',
  '_multibytecodec.cpython-35m-x86_64-linux-gnu.so'),
 (567500, 2283, 6504, 1, 'b', '_opcode.cpython-35m-x86_64-linux-gnu.so'),
 (569783, 42652, 114400, 1, 'b', '_ssl.cpython-35m-x86_64-linux-gnu.so'),
 (612435, 30180, 66992, 1, 'b', 'libbz2.so.1.0'),
 (642615, 1175887, 2711616, 1, 'b', 'libcrypto.so.1.1'),
 (1818502, 62414, 170128, 1, 'b', 'libexpat.so.1'),
 (1880916, 80229, 154376, 1, 'b', 'liblzma.so.5'),
 (1961145, 1778174, 4580776, 1, 'b', 'libpython3.5m.so.1.0'),
 (3739319, 128407, 309168, 1, 'b', 'libreadline.so.7'),
 (3867726, 178416, 442984, 1, 'b', 'libssl.so.1.1'),
 (4046142, 62857, 170776, 1, 'b', 'libtinfo.so.5'),
 (4108999, 55080, 105088, 1, 'b', 'libz.so.1'),
 (4164079, 11666, 31688, 1, 'b', 'readline.cpython-35m-x86_64-linux-gnu.so'),
 (4175745, 4876, 15432, 1, 'b', 'resource.cpython-35m-x86_64-linux-gnu.so'),
 (4180621, 8480, 25032, 1, 'b', 'termios.cpython-35m-x86_64-linux-gnu.so'),
 (4189101, 213229, 788724, 1, 'x', 'base_library.zip'),
 (4402330, 1506598, 1506598, 0, 'z', 'PYZ-00.pyz')]
?
Extract the main function:
? x gitpasshist
to filename? gitpasshist.raw
? q
The extracted file is .pyc with missing header. Let's fix that:
$ file gitpasshist.raw
gitpasshist.raw: data
$ echo -n 170d0d0a707969300f270000 | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie'  > pyc_hdr
$ cat pyc_hdr gitpasshist.raw > gitpasshist.pyc
$ file gitpasshist.pyc
gitpasshist.pyc: python 3.5.2+ byte-compiled
Now decompile .pyc to .py with uncompyle6:
uncompyle6 gitpasshist.pyc > gitpasshist.py
$ file gitpasshist.py
gitpasshist.py: Python script, ASCII text executable
$ wc -l gitpasshist.py
      67 gitpasshist.py
That's it, now there's a pure Python script.