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 ;)
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()
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.
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.