AeroCTF 2019|pycryptor2

#AeroCTF2019

https://ctftime.org/task/7765

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

どうやったらこんなに、という読みにくいコードが渡される。解読していく

import sys
import random
import struct
from hashlib import md5

p_8 = lambda val: struct.pack("!B", val)
p_16 = lambda val: struct.pack("!H", val)
p_32 = lambda val: struct.pack("!L", val)

u_8 = lambda val: struct.unpack("!B", val)[0]
u_16 = lambda val: struct.unpack("!H", val)[0]
u_32 = lambda val: struct.unpack("!L", val)[0]


class Crc:
    m_poly = 0x1021
    m_initial = 0xFFFF
    m_table = None

    def __init__(self, initial=0, poly=0):
        if initial:
            self.m_initial = initial

        if poly:
            self.m_poly = poly

        self.genTable()

    def intialVal(self, byte):
        crc = 0
        byte = byte << 8

        for j in range(8):

            if (crc ^ byte) & 0x8000:
                crc = (crc << 1) ^ self.m_poly
            else:
                crc = crc << 1

            byte = byte << 1

        return crc

    def genTable(self):
        self.m_table = [self.intialVal(i) for i in range(256)]

    def update(self, crc, byte):
        cc = 0xFF & byte

        tmp = (crc >> 8) ^ cc
        crc = (crc << 8) ^ self.m_table[tmp & 0xFF]
        crc = crc & 0xFFFF

        return crc

    def crc(self, buf):
        crc = self.m_initial

        for c in buf:
            crc = self.update(crc, ord(c))

        return crc

    def __call__(self, buf):
        return self.crc(buf)


class Key:
    m_key = None
    m_pad = 0x00

    def __init__(self, key):
        self.m_key = key

        if not self.checkKeyLen():
            self.expandKey()

    def checkKeyLen(self):
        return len(self.m_key) == 32

    def expandKey(self):
        if len(self.m_key) < 32:
            countOfBytes = 32 - len(self.m_key)
            self.m_key += p_8(self.m_pad) * countOfBytes

        return True

    def getKey(self):
        return self.m_key


class Cipher:
    m_table = []
    m_buf = ""
    m_blocks = []
    m_key = None
    m_encFileName = ""

    def __init__(self, buf, key, filename):
        self.m_buf = buf
        self.m_key = key
        self.m_encFileName = filename + ".enc"

        self.initTable()

    def initTable(self):
        seed = self.m_key.getKey()
        seed = u_32(seed[:4])

        random.seed(seed)

        for i in range(32):
            self.m_table.append(random.randint(0, 255))

    def viewTable(self):
        print self.m_table

    def parseData(self):
        i = 0

        while i < len(self.m_buf):
            self.m_blocks.append(self.m_buf[i : i + 32])
            i += 32

        for i in range(len(self.m_blocks)):
            if len(self.m_blocks[i]) != 32:
                padding = 32 - len(self.m_blocks[i])
                self.m_blocks[i] += p_8(0x0) * padding

    def getGamma(self, block):
        gamma = ""

        crc = Crc()
        block_crc = p_16(crc(block))

        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

    def writeBlock2File(self, block):
        fd = open(self.m_encFileName, "ab")
        fd.write(block)
        fd.close()

    def encode(self):
        self.parseData()

        for i in range(len(self.m_blocks)):
            enc_block = ""
            block = self.m_blocks[i]
            gamma = self.getGamma(block[:16])
            crc = Crc()(block)

            for j in range(len(gamma)):
                enc_block += chr(ord(block[j]) ^ ord(gamma[j]))

            gamma = self.getGamma(block[16:])

            for j in range(16, len(block)):
                enc_block += chr(ord(block[j]) ^ ord(gamma[j % 16]))

            enc_block += p_16(crc)

            self.writeBlock2File(enc_block)

    def decode(self):
        """oooops....."""


def GetFileData(filename):
    fd = None

    try:
        fd = open(filename, "rb")
    except:
        print "[-] Error in file open!"
        sys.exit(-2)

    buf = ""
    buf = fd.read()

    fd.close()

    if buf == "":
        print "File is empty!"
        sys.exit(-3)
    else:
        return buf


def checkKeySymbols(key):
    for i in key:
        if i not in "01234567890abcdef":
            return True

    return False


if __name__ == "__main__":

    if len(sys.argv) > 2:
        filename = sys.argv[1]
        key = sys.argv[2]
    else:
        print "Usage python " + sys.argv[0] + " <filename> <key>"
        sys.exit(-1)

    key = md5(key).hexdigest()

    if len(key) != 32:
        print "[-] Error key len!"
        sys.exit(-1337)

    if checkKeySymbols(key):
        print "[-] Error key symbols!"
        sys.exit(-31337)

    fileData = GetFileData(filename)

    key = Key(key)
    cipher = Cipher(fileData, key, filename)

    cipher.encode()

動作としては

  • 平文と鍵のファイルを受け取る

  • 鍵はMD5をとる

  • 暗号化する

という感じ。鍵をKeyクラスのインスタンスにしているが特に意味はない(MD5のhexdigestをとって置きながら長さとか文字とかの制約を確認するの流石に意味がない)。

というわけで暗号化方式を見ていく。

  • Cipher.parse で平文を32バイトのブロックに分割する。もし末尾のブロックが32バイト未満なら0で埋める。

  • 各ブロックについて、先頭16バイトと末尾16バイトに分割し、それぞれGamma値を計算してxorしたものを暗号文とする。32バイトの暗号文の後ろに2バイトのCRC値(これは平文32バイトのCRC)を加える

なるほど。CRCとGammaの制約から平文が定まるのかな。

CRCの計算を見る。まあどういう演算が行われているかはわからないが、とにかくCRC値は初期値(m_poly)と入力値で決まりそうだということがわかった。逆算とかはできなさそうな感じ。

Gammaの計算を見る。

  • 受け取ったblockのCRC値を計算する

  • blockの各バイトについて

  • CRCの i mod 2の値をとってきて1bit分右シフトする(7bitになる)

  • keyのi番目の文字をとってきてこれとxor(やっぱりこれも7bit)(keyは32文字あるのに16文字しか使われない)

  • できた7bitの値を文字として連結したものがGamma

うーんなるほど? keyは各文字のパターンが16通りしかなくて先頭16文字しか使われないので総当りできるかなと思ったけど 16 ** 16は無理

ただ、暗号化は究極のところ平文と鍵を暗号化しているだけなので、平文の一部がわかればそのときのGamma値がわかる。平文の一部はわかっているのでGamma値と合わせてKeyがわかる。Gamma値はKeyとCRCのxorなのでKeyがわかるとありえるすべてのGammaを列挙可能になる。まあ 65535 * 4 くらい。

ありうるGammaを列挙できたらCRCが合うかどうかを見ながら32バイトずつ復号できそう。 65535 * 4 ** 2 くらいかかるけど……