[WARGAME NDH 2016] Write-Up – Crypto: SuperCipher

07
Jul
2016
  • Google Plus
  • LinkedIn
  • Viadeo
Posted by: Yann C.  /   Category: Cryptanalyze / Cryptography / Cryptology / / / / / Reverse-Engineering /   /   No Comments

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.

 

decompyle

decompyle

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):

timestamp

timestamp

“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 :

  • Google Plus
  • LinkedIn
  • Viadeo
Author Avatar

About the Author : Yann C.

Consultant en sécurité informatique et s’exerçant dans ce domaine depuis le début des années 2000 en autodidacte par passion, plaisir et perspectives, il maintient le portail ASafety pour présenter des articles, des projets personnels, des recherches et développements, ainsi que des « advisory » de vulnérabilités décelées notamment au cours de pentest.