I was solving this task for 3-4 days after TJCTF and yesterday finally solved it.
We are given a link to µ'sic.zip file.
Archive is protected with a password. I've brutted the pasword to the archive.
Password is "LL". Inside the archive a Welcome.txt file with the next message is stored:
Welcome to µ'sic!
The purpose of this challenge is to elucidate the applications of forensics and steganography techniques and the manner in which they may be solved. Our intent is that you will learn and develop a multitude of skills as you progress through this challenge.
For each "layer" of this challenge, we have included one of our favorite songs from Love Live! (now you know what LL stands for), along with the cover image that came with the CD for each song. Each layer involves a password protected zip file, and the password can be found hidden in the cover image.
If nothing else, please enjoy the µ'sic!
So we have several nested archives with songs, posters and some other files.
Each level's cover image contains a hidden password to the next level's zip archive.
Password is hidden as a plain text in the cover image:
We have two cover images. We can find diff bytes using Python script:
with open('1.jpg', 'rb') as f: data1 = f.read() with open('2.jpg', 'rb') as f: data2 = f.read() out = '' for i in xrange(len(data1)): if data1[i] != data2[i]: out += data1[i] print out
And get a next data:
Which is base64 encoded next level's password:
Inside this level's achive we have a PDF file.
Using the next script I've extracted and decompressed all stream data blocks from PDF.
import zlib with open('1.pdf', 'rb') as f: data = f.read() while True: start = data.find('\x0Astream\x0A') if start < 0: break data = data[start + 8:] end = data.find('\x0Aendstream') try: decompressor = zlib.decompressobj() dec_data = decompressor.decompress(data[:end]) with open('out.png', 'wb') as f: f.write(dec_data) except: pass
Is is password written with Wingdings font.
In this level's archive we have two PNG images. Using the next script I've extracted diff image with a password:
import operator from PIL import Image def diff(col1, col2): return map(abs, map(lambda x: operator.sub(*x), zip(col1, col2))) img1 = Image.open('1.png') pixels1 = img1.load() img2 = Image.open('2.png') pixels2 = img2.load() w, h = img1.size img3 = Image.new('RGB', (w, h)) pixels3 = img3.load() for y in xrange(h): for x in xrange(w): v = tuple(diff(pixels1[x, y], pixels2[x, y])) if v == (1, 1, 1): pixels3[x, y] = (255, 255, 255) else: pixels3[x, y] = v img3.save('out.png')
We have a jpg file. I've noticed that inside of this file, zip achive is hidden. With the next script - I've extracted it.
with open('Cutie Panther.jpg', 'rb') as f: data = f.read() pos = data.find('PK') with open('out.zip', 'wb') as f: f.write(data[pos:])
Inside extracted archive - Cutie Panther.png image with QR codes... Lots of them...
Image size is 13500 x 13500 pixels. 900 QR codes.
So I've cutted the image into subimages with QR codes. And then decoded each of them.
import glob import os import re import subprocess import zipfile from PIL import Image def cut_qr_codes(img_path, out_dir, qr_left, qr_top, qr_w, qr_h, qr_step_x, qr_step_y): """ Cut off subimages from image """ if not os.path.exists(out_dir): os.makedirs(out_dir) img = Image.open(img_path) img_w, img_h = img.size index = 0 for j in xrange(qr_top, img_h - qr_top - qr_step_y, qr_h + qr_step_y): for i in xrange(qr_left, img_w - qr_left - qr_step_x, qr_w + qr_step_x): print i, j, index img.crop((i, j, i + qr_w, j + qr_h)).save(os.path.join(out_dir, '%003d.png' % index)) index += 1 def decode_qr(path): """ Decode QR using zbar """ res = subprocess.check_output('zbarimg %s -q' % path, shell=True) return re.findall('QR\-Code:(.*)\r', res) def unzip_specific_file(zip_file, name, password): """ Unzip file from zip archive """ try: zip_file.extract(name, pwd=password) return True except: pass return False def main(): # cut image into subimages with QR codes out_dir = './qr_codes/' cut_qr_codes('Cutie Panther.png', out_dir, 40, 40, 371, 371, 79, 79) # decode QR codes passwords =  for fn in glob.glob(os.path.join(out_dir, '*.png')): data = decode_qr(fn) passwords.append(data) # find the password filename = 'zip5.zip' zip_file = zipfile.ZipFile(filename) # get image's name in archive name = '' for name in zip_file.namelist(): if name.endswith('.jpg'): print name break for password in passwords: if unzip_specific_file(zip_file, name, password): print 'password is:', password break if __name__ == '__main__': main()
After running this script, I've found appropriate password.
import itertools from collections import Counter from PIL import Image img = Image.open('Octal.png') w, h = img.size pixels = img.load() colors_data =  for y in xrange(0, h, 20): for x in xrange(0, w, 20): col = pixels[x, y] colors_data.append(col) alphabet = set(colors_data) alphabet_len = len(alphabet) out_file = open('out.dat', 'wb') colors = map(str, xrange(8)) for items in itertools.product(colors, repeat=alphabet_len): # get only possible variants without repetition if len(set(items)) == alphabet_len: alpha_to_item = dict(zip(alphabet, items)) # get image as an octal number oct_number = ''.join(alpha_to_item.get(x) for x in colors_data) dec_number = int(oct_number, 8) # write all possible results if len(hex(dec_number)[2:-1]) % 2 != 0: out_file.write(('0' + hex(dec_number)[2:-1]).decode('hex') + '\n') out_file.close()
After running the script I've checked the file with huge number of possible results. And one of them was:
So a first part of this message is a password:
On this level - a password was simply hidden in JPG file as plain text.
In this level we have a PNG file.
I've noticed a hint in PNG file after IEND block: r^g&b
And using the next script I've got a password to the next level:
from PIL import Image img = Image.open('1.png') w, h = img.size pixels = img.load() bits = '' img_out = Image.new('RGB', (w, h)) img_out_pixels = img_out.load() for j in xrange(h): for i in xrange(w): col = pixels[i, j] r, g, b = col bits += '1' if (r^g&b) & 0b1 else '0' img_out_pixels[i, j] = (255, 255, 255) if ((r ^ g & b) & 0b1) else (0, 0, 0) break bits = '0' + bits # !!!! out = '' for i in xrange(0, len(bits), 8): out += chr(int(bits[i:i + 8], 2)) print `out`
The last archive contained Flag.png file with part of QR code. Only a part :(
I've decoded QR code by hands :)
Here is the structure of QR code, I've filled each data blocks with color:
It is QR code version 4 (33x33). As you can see the mask (x % 3) is used here.
First block = 4 or byte encoding (8 bits per character).
Second block is a encoded message length = 58.
Next four blocks in the first column are t, j, c, t.
In such way all 58 bytes of data was extracted.
It was a really difficult task by Annabel Faylan. Thank you!
Inception! We need to go deeper! :)