Rating:

Aero CTF 2019

pycryptor v2

Information

Category:Points:Writeup Author
Crypto5001MiKHalyCH1

Description:

Ещё одна реализация защиты наших чертежей. Опять на Python. На этот раз, нам сказали, что всё серьёзно.

Another implementation of the protection of our drawings. Again in Python. This time, we were told that everything is serious.

Solution

This algo implementation looks really hard. Let's figure it out.

  1. From Crc class we only need to know that calculated crc of block is 2 bytes long.

  2. Key class is simple. It expands Your key to 32 bytes. But our key always is md5(input_key) - 16 bytes - 32 hex symbols.

  3. Cipher class encrypts file this way:

  • Splits the hole file into blocks 32 bytes long and exapands the last block with zero bytes.
  • Splits each block into 2 subblocks 16 bytes length.
  • For each block calculates 16 bytes long gamma:
def getGamma(self, block):
   gamma = ''

   block_crc = p_16(Crc()(block)) # 2 bytes

   for i in range(16):
       val = ord(block_crc[i%2])
       val >>= 1
       val ^= ord(self.m_key.getKey()[i])
       val &= 0xff

       gamma += chr(val)

   return gamma

Than it encrypts each subblock like this:

enc_subblock = xor_bytes(subblcok, gamma)
  • So the encrypted block looks like this (where || is concatenation of bytes):
enc_block = enc_subblock_1 || enc_subblock_2 || p_16(crc(block))

Key recovery

From getGamma function we can understand that we really use just first 16 bytes of the key.

We can unfold gamma like this:

bytes_crc = p_16(crc(subblock))
for i in range(16):
    enc[i] = subblock[i] ^ key[i] ^ (bytes_crc[i%2] >> 1)

Beacuse of XOR we can move key[i] to the left part of equation and enc[i] to the right.

I assumed that the original image is in jpg format. So we know first 13 bytes of it. We can brute last 3 bytes to recover key.

enc = read_file("test_image.enc")
for tail in generate_tails():
    jpg_header = ("ffd8ffe000104a464946000101" + tail).decode("hex")

    key = decrypt_key(jpg_header, enc[:16])
    if all(x in hexdigits for x in key):
        print "Key:", key
        print "Tail:", tail
        print

And now we know that first 16 bytes of key are "ed40f1e93ab22ea7".

Don't forget that last block padding are zeros. So we can recover key faster:

with open("test_image.enc", "rb") as f:
    block = f.read()[-18:-2]
block.encode("hex")

pt = "\0" * 16 # jpg ends with FF D9. But we are lucky that the hole last subblock contains in padding
key = p_16(Crc()(pt))
key = "".join(chr(ord(x) >> 1) for x in key)
print xor_bytes(xor_bytes(block, key), pt)

Gamma calculation

Now we know the key. We can precalculate all possible gammas because it depends on p_16(crc(subblock)) (which is 2 bytes long) and key.

And because of right shifting of crc bytes each gamma can be calculated from 4 different crc's.

def generate_gammas():
    for x in range(0x80):
        for y in range(0x80):
            gamma = ''
            block_crc = [x, y]

            for i in range(16):
                val = block_crc[i % 2]
                val ^= ord(KEY[i])
                val &= 0xff
                gamma += chr(val)

            _x = x << 1
            _y = y << 1

            a = _x << 8 | _y
            b = _x << 8 | (_y | 1)
            c = (_x | 1) << 8 | _y
            d = (_x | 1) << 8 | (_y | 1)
            crcs = set(map(p_16, {a, b, c, d}))

            yield gamma, crcs

Full decoding

Now we can recover all subblocks. Let's iterate through all gammas and XOR them with our encrypted subblock. If crc(decrypted_subblock) is one of 4 gamma crc's then our decrypted_subblock is one of possible candidates to be the real subblock.

At this step we have some variants for first and for second subblock of full block. We can find the right one by calculating crc(subblock_1 || sublock_2) and compare it with crc(real_block).

def decrypt_block_part(block, gammas):
    for gamma, gamma_crcs in gammas:
        decrypt_block = xor_bytes(block, gamma)
        real_block_crc = p_16(calculate_crc(decrypt_block))

        if real_block_crc in gamma_crcs:
            yield decrypt_block

def decrypt_block(block, crc, gammas):
    second_parts = list(decrypt_block_part(block[16:], gammas))

    for first_part in decrypt_block_part(block[:16], gammas):
        for second_part in second_parts:
            dec_block = first_part + second_part

            if CRC(dec_block) == crc:
                return dec_block

My exploit works for me in about 2 hours. The task was done.

P.S.

I'd like to say thanks very much to author for the very interesting hard task.

Original writeup (https://github.com/VoidHack/write-ups/tree/master/Aero%20CTF%202019/crypto/pycryptor%20v2).