Write-up of the challenge “Crypto – SuperCipher” of Nuit du Hack 2016 Wargame
The weekend of 02-03 july 2016 is the WARGAME of the Nuit du Hack 2016 as a Jeopardy CTF. Having had the opportunity and the time to participate with some colleagues and friends, here’s a write-up resolution of the challenges which we could participate.
- Category: Crypto
- Name: SuperCipher
- Description : So easy
- URL : sp.wargame.ndh
- Points : 100
tl;dr : Download and decompile home.pyc file, then extract timestamp of “secret” file (seed) to uncipher
This challenge was in the form of a web interface, allowing:
- Upload a file to encrypt: a ZIP archive output with two files “secret” and “key”
- Upload an encrypted file, specify a key, and retrieve the plaintext version
- Download “flag.zip” that had been encrypted via this service
It is a pure and simple encryption service on line.
Watching the contents of the “flag.zip”
root@kali 12:22 [~/ndh2k16/SuperCipher] # unzip flag.zip Archive: flag.zip creating: secret/ inflating: secret/key extracting: secret/secret
Could it be so simple? See the contents of these files:
secret/secret :
/8bAieboX5pFq1sI6js92nrI6huZoxLZ5A==
secret/key :
QUhBSEFILVRISVMtSVMtTk9ULVRIRS1LRVk=
Decode the key:
root@kali 12:24 [~/ndh2k16/SuperCipher] # cat secret/key | base64 -d AHAHAH-THIS-IS-NOT-THE-KEY
Ok, well we had a “secret” but the key is still unknown …
Page performing the encryption / decryption processing is called “home.py”. Sounds like Python. Try to recover the compiled version (home.pyc) script: bingo! It was the compiled version.
With the compiled version (*.pyc) script, it is necessary to recover the original Python source code. For that different solutions exist as “uncompyle2“, “decompyle ++“, etc. In our case we opted for a solution on Windows “Easy Python Decompiler” (which ultimately uses either of the previous binary).
Launch tool, drag’n’drop the * .pyc file and a “home.pyc_dis” file with the Python code is automatically generated.
Python source code:
# Embedded file name: home.py from bottle import route, run, template, request, static_file import time import random import base64 import zipfile from StringIO import StringIO import os, sys from Crypto.Cipher import AES from Crypto import Random class AESCipher: def __init__(self): self.key = 'FOOBARBAZU123456' self.pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) self.unpad = lambda s: s[:-ord(s[len(s) - 1:])] def encrypt(self, raw): raw = str(raw) raw = self.pad(raw) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) return base64.b64encode(iv + cipher.encrypt(raw)) def decrypt(self, enc): enc = base64.b64decode(enc) iv = enc[:16] cipher = AES.new(self.key, AES.MODE_CBC, iv) return self.unpad(cipher.decrypt(enc[16:])) @route('/') @route('/home.py') def hello(): return '\n<h1>SuperCipher</h1>\n<h2>Chiffrer :</h2>\n<form action="/home.py/secret.zip" method="post" enctype="multipart/form-data">\n Select a file: <input type="file" name="upload" />\n <input type="submit" value="cipher" />\n</form>\n<br />\n<h2>Dechiffrer</h2>\n<form action="/home.py/uncipher" method="post" enctype="multipart/form-data">\n Password: <input type="password" name="key" />\n Select a file: <input type="file" name="upload" />\n <input type="submit" value="uncipher" />\n</form>\n' @route('/home.py/secret.zip', method='POST') def cipher(): seed = int(time.time()) random.seed(seed) upload = request.files.get('upload') upload_content = upload.file.read() content_size = len(upload_content) mask = ''.join([ chr(random.randint(1, 255)) for _ in xrange(content_size) ]) cipher = ''.join((chr(ord(a) ^ ord(b)) for a, b in zip(mask, upload_content))) b64_cipher = base64.b64encode(cipher) aes = AESCipher() key = aes.encrypt(seed) secret = StringIO() zf = zipfile.ZipFile(secret, mode='w') zf.writestr('secret', b64_cipher) zf.writestr('key', key) zf.close() secret.seek(0) return secret @route('/home.py/uncipher', method='POST') def cipher(): key = request.forms.get('key') upload = request.files.get('upload') try: aes = AESCipher() key = aes.decrypt(key) random.seed(int(key)) upload_content = base64.b64decode(upload.file.read()) content_size = len(upload_content) mask = ''.join([ chr(random.randint(1, 255)) for _ in xrange(content_size) ]) plain = ''.join((chr(ord(a) ^ ord(b)) for a, b in zip(mask, upload_content))) return plain except: return 'Uncipher error.' @route('/<filename:path>') def download(filename): return static_file(filename, root=os.path.join(os.path.dirname(sys.argv[0])), download=filename) run(host='0.0.0.0', port=8080)
Note that a key is set in the object class “AESCipher”
self.key = 'FOOBARBAZU123456'
In addition, the method of “encryption” (which produces the “secret.zip” output), initializes its encryption process via the current timestamp:
@route('/home.py/secret.zip', method='POST') def cipher(): seed = int(time.time()) random.seed(seed)
We also have access to the complete deciphering function:
@route('/home.py/uncipher', method='POST') def cipher(): key = request.forms.get('key') upload = request.files.get('upload') try: aes = AESCipher() key = aes.decrypt(key) random.seed(int(key)) upload_content = base64.b64decode(upload.file.read()) content_size = len(upload_content) mask = ''.join([ chr(random.randint(1, 255)) for _ in xrange(content_size) ]) plain = ''.join((chr(ord(a) ^ ord(b)) for a, b in zip(mask, upload_content))) return plain except: return 'Uncipher error.'
The idea is based on the content of “flag.zip” including its “secret” to be decrypted. The “seed” that was served at the time to encrypt this message containing the flag, can be deducted from the “last modified date of the secret file” (command “stat” in Linux):
“secret” was last modified the June 30, 2016 at 17h 17 min 52s. We need to convert this date to a timestamp and arbitrarily initialize the “seed” of the decryption function with this value:
t = datetime.datetime(2016, 06, 30, 17, 17, second=52) seed = int(time.mktime(t.timetuple())) #seed = 1467299872
For the final resolution, we need to create a new Python script “flag.py” inspired from the decryption function, with our “seed timestamp”:
import random import base64 import time import datetime from Crypto.Cipher import AES from Crypto import Random class AESCipher: def __init__(self): self.key = 'FOOBARBAZU123456' self.pad = lambda s: s + (16 - len(s) % 16) * chr(16 - len(s) % 16) self.unpad = lambda s: s[:-ord(s[len(s) - 1:])] def encrypt(self, raw): raw = str(raw) raw = self.pad(raw) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) return base64.b64encode(iv + cipher.encrypt(raw)) def decrypt(self, enc): enc = base64.b64decode(enc) iv = enc[:16] cipher = AES.new(self.key, AES.MODE_CBC, iv) return self.unpad(cipher.decrypt(enc[16:])) t = datetime.datetime(2016, 06, 30, 17, 17, second=52) seed = int(time.mktime(t.timetuple())) #seed = 1467299872 aes = AESCipher() key = aes.encrypt(seed) aes = AESCipher() key = aes.decrypt(key) random.seed(int(key)) upload_content = base64.b64decode("/8bAieboX5pFq1sI6js92nrI6huZoxLZ5A==") content_size = len(upload_content) mask = ''.join([chr(random.randint(1,255)) for _ in xrange(content_size)]) plain = ''.join(chr(ord(a)^ord(b)) for a,b in zip(mask, upload_content)) print "Flag : " + plain
Run :
root@kali 12:16 [~/ndh2k16] # python flag.py Flag : ndh_crypt0sh1tn3v3rch4ng3
Flag : ndh_crypt0sh1tn3v3rch4ng3
Thank you to all the team of the NDH2K16 for this event and for the whole organization!
Greeting to nj8, St0rn, Emiya, Mido, downgrade, Ryuk@n and rikelm, ? // Gr3etZ
Sources & resources :