Writeup Inshack CTF – Obscure File Format

Diberikan file obscure-file-format.zip, extract file tersebut, dan didapatkan 3 file bernama a, k, dan l.

Screenshot_20190519_085936.png

File a dan k berisi binary data yang belum diketahui formatnya apa. Sementera file l berisi python script yang codenya diobfuscate.

Screenshot_20190519_090156.png

Untuk mendapatkan script aslinya, saya membuat fungsi exec palsu yang digunakan untuk memprint string apa yang diexec setelah itu string tersebut akan dipanggil lagi dengan fungsi exec yang asli.

real_exec = exec
def exec(s):
        print(s)
        y=input("continue?")
        if 'y' in y:
                with open('out', 'wb') as f:
                   f.write(s)
                   exit()
        return real_exec(s)

Di fungsi exec palsu yang saya buat saya menambahkan script untuk menyimpan string yang diexec ke dalam file jika saya menginputkan y.

Saya menaruh script diatas sebelum fungsi exec dipanggil (lihat di gambar kedua). Jalankan scriptnya

Ketika script diatas menampilkan string yang bisa dikira itu adalah script python, saya langsung menginput y dan program berhenti. String tersebut akan disimpan di file yang bernama out. Isi dari file out nampak seperti di bawah ini.

Isi dari file tersebut adalah script python yang diobfuscate, semua variable dan fungsinya telah dirubah dengan nama yang aneh, sehingga menyulitkan seseorang untuk memahami kode tersebut. Script diatas bisa dilihat di https://pastebin.com/d2Td7NJy

Setelah membaca kode tersebut, saya mencoba mengganti semua variable, nama fungsi dan menghapus beberapa bagian kode yang tidak berguna. Di bawah ini adalah hasil kode yang telah diubah .

import os
import uuid
import zlib
import struct
import random
import pathlib
import argparse
from cryptography.hazmat import backends
from cryptography.hazmat.primitives import ciphers
import code

null_byte = bytes([0])
length = 16
length2 = 16
default_backend = backends.default_backend()

class AESCrypto(object):
    def __init__(self):
        self.random16_1 = os.urandom(length)
        self.random16_2 = os.urandom(length2)
        self.cipher = ciphers.Cipher(ciphers.algorithms.AES(self.random16_2),
                                     ciphers.modes.CBC(self.random16_1),
                                     backend=default_backend)
    def __str__(self):
        return f'[{self.random16_1.hex()}|{self.random16_2.hex()}]'
    def encrypt(self, some_data):
        encryptor = self.cipher.encryptor()
        return (encryptor.update(some_data) + encryptor.finalize())
    def get_iv_key_reversed(self):
        sumrandom_list = list((self.random16_1 + self.random16_2))
        sumrandom_list.reverse()
        return bytes(sumrandom_list)

def padding_it(data: 'bytes', length: 'int'):
    padbyte = (length - (len(data) % length))
    return (data + bytes(([padbyte] * padbyte)))

def unpad_it(data: 'bytes', length: 'int'):
    return data[0:-data[-1]]

class SomeSerializeStuff():
    bits_num = 128
    length3 = 16

    def __init__(self, info_file: 'ArchiveEntry'):
        self.info_file = info_file
        self.cryptostuff = AESCrypto()
        self.emptrydict = {

        }

    def __str__(self):
        return f'[{self.info_file.get_uuid}|{self.cryptostuff}]'

    def serializedata(self):
        if (not self.emptrydict):
            raise RuntimeError('Serializing key before data...')
        raw_buf = self.info_file.get_uuid.bytes
        raw_buf += self.cryptostuff.get_iv_key_reversed()
        raw_buf += struct.pack('I', len(self.emptrydict))
        for (key, value) in self.emptrydict.items():
            raw_buf += struct.pack('2I', key, value)
        return raw_buf

    def EncryptFile(self):
        with self.info_file.get_pathfile.open('rb') as fileds:
            content_file = fileds.read()
        padded_contentfile = padding_it(zlib.compress(content_file), SomeSerializeStuff.length3)
        padded_content_encryptedfile = padding_it(self.cryptostuff.encrypt(padded_contentfile), SomeSerializeStuff.bits_num)
        len_div_bit = (len(padded_content_encryptedfile) // SomeSerializeStuff.bits_num)
        shuffled_bit = list(range(len_div_bit))
        random.shuffle(shuffled_bit)
        for (left, right) in zip(list(range(len_div_bit)), shuffled_bit):
            self.emptrydict[left] = right
        extracted_content = [padded_content_encryptedfile[(j * SomeSerializeStuff.bits_num):((j + 1) * SomeSerializeStuff.bits_num)] for j in range(len_div_bit)]
        final = bytes([])
        for i in range(len_div_bit):
            final += extracted_content[self.emptrydict[i]]
        return final

class get_info_file():

    def __init__(self, pathfile: 'pathlib.Path'):
        self.pathfile = pathfile
        self.uuid = uuid.uuid4()
        self.file_stat = pathfile.stat()

    def __str__(self):
        return f'[{self.uuid}|{self.pathfile}]'

    @property
    def get_uuid(self):
        return self.uuid

    @property
    def get_pathfile(self):
        return self.pathfile

    @property
    def get_pathfile_str(self):
        return str(self.pathfile)

    @property
    def get_st_size(self):
        return self.file_stat.st_size

    @property
    def get_st_mode(self):
        return self.file_stat.st_mode

    @property
    def get_st_atime(self):
        return self.file_stat.st_atime

    @property
    def get_st_mtime(self):
        return self.file_stat.st_mtime

    @property
    def get_st_ctime(self):
        return self.file_stat.st_ctime

    def get_infofile_as_buf(self):
        pathname_as_byte = (self.get_pathfile_str.encode() + null_byte)
        buf = struct.pack('I', len(pathname_as_byte))
        buf += pathname_as_byte
        buf += self.get_uuid.bytes
        buf += struct.pack('2I', self.get_st_size, self.get_st_mode)
        buf += struct.pack('3d', self.get_st_atime, self.get_st_mtime, self.get_st_ctime)
        return buf

class KeyStore():
    raw_buf5 = bytes([76, 48, 76, 75, 83, 84, 82, 0])

    def __init__(self):
        self.nillist = []

    def get_some_serializestuff(self, info_file: 'ArchiveEntry'):
        someserializestuff = SomeSerializeStuff(info_file)
        print(someserializestuff)
        self.nillist.append(someserializestuff)
        return someserializestuff

    def create_keystore(self, pathh: 'pathlib.Path'):
        raw_buf5 = KeyStore.raw_buf5
        raw_buf5 += struct.pack('I', len(self.nillist))
        for itemlist in self.nillist:
            raw_buf5 += itemlist.serializedata()
        pathh.joinpath('keystore').write_bytes(raw_buf5)

class CryptoStuff():
    raw_buf = bytes([76, 48, 76, 65, 82, 67, 72, 0])
    limit_size = 1048576

    def __init__(self):
        self.emptrylist = []
        self.keystore = KeyStore()

    def process_infofile_and_serializestuff(self, filepath: 'pathlib.Path'):
        info_file = get_info_file(filepath)
        print(info_file)
        if (info_file.get_st_size > CryptoStuff.limit_size):
            raise RuntimeError(f'{info_file.get_pathfile} size is above the limit ({CryptoStuff.limit_size})!')
        serializestuff = self.keystore.get_some_serializestuff(info_file)
        self.emptrylist.append((info_file, serializestuff))

    def process_ancrypt_all(self, path: 'pathlib.Path'):
        raw_buf = CryptoStuff.raw_buf
        raw_buf += struct.pack('I', len(self.emptrylist))
        for (info_file, serializestuff) in self.emptrylist:
            print(f'adding {info_file.get_pathfile}...')
            raw_buf += info_file.get_infofile_as_buf()
            encrypted_datafile = serializestuff.EncryptFile()
            raw_buf += struct.pack('I', len(encrypted_datafile))
            raw_buf += encrypted_datafile
        path.joinpath('archive').write_bytes(raw_buf)
        self.keystore.create_keystore(path)

class DoingPathStuff():

    def __init__(self, path: 'pathlib.Path'):
        self.pathf = pathlib.Path(path)

    def get_list_file(self, recrusive: 'bool'):
        listfiles = list(self.pathf.glob('*'))
        if recrusive:
            listfiles = list(self.pathf.rglob('*'))
        return list(filter((lambda i: i.is_file()), listfiles))

def parsearg():
    arg = argparse.ArgumentParser(description='')
    arg.add_argument('d')
    arg.add_argument('o')
    return arg.parse_args()

def main():
    argparsed = parsearg()
    generatedbyte = CryptoStuff()
    pathstuff = DoingPathStuff(argparsed.d)
    p = pathlib.Path(argparsed.o)
    for filename in pathstuff.get_list_file(True):
        generatedbyte.process_infofile_and_serializestuff(filename)
    p.mkdir(parents=True, exist_ok=True)
    generatedbyte.process_ancrypt_all(p)

if (__name__ == '__main__'):
    main()

Walaupun belum begitu jelas beberapa fungsi dan variable yang saya ubah namanya, tetapi setidaknya saya cukup bisa memahami kode tersebut.

Script diatas membutuhkan 2 argumen. Argumen pertama adalah nama folder input, nantinya semua file-file yang ada di dalam folder tersebut akan diencrypt. Argumen kedua adalah nama folder output, nantinya file2 yang akan diencrypt akan ditaruh di folder tersebut.

Untuk mengetesnya, saya membuat 1 contoh folder yang berisi 1 file yang isinya adalah sebuah string yang random.

Selanjutnya saya menjalankan script diatas. Saya menggunakan folder ngetes sebagai argumen pertama dan ngetes_out sebagai argumen kedua (sebagai folder hasil)

Hasilnya adalah terdapat folder baru bernama ngetes_out yang berisi file yang bernama archive dan keystore.

Setelah diperhatikan, byte2 awal pada file archive mirip dengan byte2 awal dengan file a (file awal yang berasal dari challenge)

Dan, byte2 awal pada file keystore mirip degan byte2 awal dengan file k (file challenge)

Jadi bisa kita simpulkan, file a merupakan file archive dan file k merupakan file keystore.

Setelah melakukan reverse algoritma pada script diatas, saya membuat script solver yang dapat melakukan proses decrypting. dan mengextract semua file yang telah didecrypt. Dibawah berikut merupakan script solvernya.

import os
import uuid
import zlib
import struct
import random
import pathlib
import argparse
import sys
from cryptography.hazmat import backends
from cryptography.hazmat.primitives import ciphers
import code
import pprint

null_byte = bytes([0])
length = 16
length2 = 16
bits_num = 128
default_backend = backends.default_backend()

def unpad_it(data: 'bytes', length: 'int'):
    return data[0:-data[-1]]

class AESDecrypt(object):
    def __init__(self, key, iv):
        self.key = key
        self.iv = iv
        self.cipher = ciphers.Cipher(ciphers.algorithms.AES(self.key),
                                     ciphers.modes.CBC(self.iv),
                                     backend=default_backend)
    def decrypt(self, some_data):
        decryptor = self.cipher.decryptor()
        return (decryptor.update(some_data) + decryptor.finalize())

def get_infofile_from_fd(iofd):
    lenpath = struct.unpack("I", iofd.read(4))[0]
    filename = iofd.read(lenpath)
    raw_uuid = iofd.read(16)
    st_size, st_mode = struct.unpack("2I", iofd.read(2*4))
    st_atime, st_mtime, st_ctime = struct.unpack("3d", iofd.read(8*3))
    return dict(lenpath=lenpath, filename=filename,
                uuid=uuid.UUID(bytes=raw_uuid), st_size=st_size,
                st_mode=st_mode, st_atime=st_atime, st_mtime=st_mtime,
                st_ctime=st_ctime)

def get_infokey(iofd):
    emptry_dict = {}
    raw_uuid = iofd.read(16)
    keyivred = list(iofd.read(16*2))
    keyivred.reverse()
    iv = bytes(keyivred[:16])
    key = bytes(keyivred[16:])
    count_dict = struct.unpack("I", iofd.read(4))[0]
    for i in range(count_dict):
        k, value = struct.unpack("2I", iofd.read(4*2))
        emptry_dict[k] = value
    return dict(uuid=uuid.UUID(bytes=raw_uuid), iv=iv, key=key,
                               emptry_dict=emptry_dict)

class ParseKeyStore(object):
    def __init__(self, path: 'pathlib.Path'):
        self.keydict = {}
        self.path = path

    def read_keystore(self):
        p = self.path.joinpath('keystore')
        with p.open(mode='rb') as fk:
            magic = fk.read(8)
            count_key = struct.unpack("I", fk.read(4))[0]
            for i in range(count_key):
                infokey = get_infokey(fk)
                #pprint.pprint(infokey)
                self.keydict[infokey['uuid']] = infokey
            return self.keydict

def DecryptFile(infofile, infokey, data, len_data):
    cipher = AESDecrypt(infokey['key'], infokey['iv'])
    length3 = 16
    len_div_bit = len_data // bits_num
    data_as_list = [data[(j * bits_num): ((j + 1) * bits_num)] for j in range(len_div_bit)]
    real_content_encrypted = [bytes()]*len_div_bit
    for i in range(len_div_bit):
        real_content_encrypted[infokey['emptry_dict'][i]] = data_as_list[i]
    real_content_encrypted = unpad_it(b''.join(real_content_encrypted), bits_num)
    decrypted = cipher.decrypt(real_content_encrypted)
    uncompressed = zlib.decompress(unpad_it(decrypted, length3))
    newfilename = infofile['filename'].decode()[:-1].strip().replace('/', '.') # I'm to lazy to doing path stuff, so replace all slash to dot
    with open(newfilename, 'wb') as fp:
        fp.write(uncompressed)
    print(uncompressed)

class ParseArchive(object):
    raw_buf = bytes([76, 48, 76, 65, 82, 67, 72, 0])
    limit_size = 1048576

    def __init__(self, path: 'pathlib.Path'):
        self.path = path
        self.emptrylist = []
        self.keystore = ParseKeyStore(path)

    def process_decrypt(self):
        p = self.path.joinpath('archive')
        keylist = self.keystore.read_keystore()
        with p.open(mode='rb') as fa:
            magic = fa.read(8)
            count_file = struct.unpack("I", fa.read(4))[0]
            for i in range(count_file):
                info = get_infofile_from_fd(fa)
                #pprint.pprint(info)
                len_encrypted = struct.unpack("I", fa.read(4))[0]
                encrypted_content = fa.read(len_encrypted)
                DecryptFile(info, keylist[info['uuid']], encrypted_content, len_encrypted)

def main():
    os.chdir(sys.argv[1])
    p = pathlib.Path('.')
    archive = ParseArchive(p)
    archive.process_decrypt()
    return

if __name__ == '__main__':
    main()

Script solve ini membutuhkan argumen berupa nama folder yang didalamnya terdapat file archive dan keystore agar dapat melakukan proses decrypting.

Jadi, saya membuat folder bernama flag, pindahkan file a (dari challenge) ke dalam folder flag dengan nama archive, dan file k dengan nama keystore.

Jalankan script solver diatas.

$ python3 solve.py flag

Selanjutnya, file2 yang telah didecrypt akan terdapat di dalam folder flag itu sendiri.

Terdapat banyak file yang berawalan tmp, dengan mengetahui format flag yang berawalan INSA kita dapat dengan mudah mendapatkan flagnya dengan menggrep semua file yang ada disana.

Flag : INSA{9c431db9206d2c13bd730a331f07561e49fdebb13ef13057bbeee655a6808fa5}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s