This was one of the warm-up crypto challenges for the Pwn2Win 2020 CTF. This is a block cipher oracle challenge, although the heart of the challenge is really taking the time to understand the code. You connect to server.py which gives you two options:
The first option will take your base64 encoded input plaintext and give you the corresponding ciphertext. The second option gives you the encrypted flag. The important parts of code are as follows:
import sys
import base64
from Crypto.Cipher import AES
from secrets import flag, key1, iv1
def to_blocks(txt):
return [txt[i*BLOCK_SIZE:(i+1)*BLOCK_SIZE] for i in range(len(txt)//BLOCK_SIZE)]
def xor(b1, b2=None):
if isinstance(b1, list) and b2 is None:
assert len(set([len(b) for b in b1])) == 1, 'xor() - Invalid input size'
assert all([isinstance(b, bytes) for b in b1]), 'xor() - Invalid input type'
x = [len(b) for b in b1][0]*b'\x00'
for b in b1:
x = xor(x, b)
return x
assert isinstance(b1, bytes) and isinstance(b2, bytes), 'xor() - Invalid input type'
return bytes([a ^ b for a, b in zip(b1, b2)])
BUFF = 256
BLOCK_SIZE = 16
iv2 = AES.new(key1, AES.MODE_ECB).decrypt(iv1)
key2 = xor(to_blocks(flag))
def encrypt(txt, key, iv):
global key2, iv2
assert len(key) == BLOCK_SIZE, f'Invalid key size'
assert len(iv) == BLOCK_SIZE, 'Invalid IV size'
assert len(txt) % BLOCK_SIZE == 0, 'Invalid plaintext size'
bs = len(key)
blocks = to_blocks(txt)
ctxt = b''
aes = AES.new(key, AES.MODE_ECB)
curr = iv
for block in blocks:
ctxt += aes.encrypt(xor(block, curr))
curr = xor(ctxt[-bs:], block)
iv2 = AES.new(key2, AES.MODE_ECB).decrypt(iv2)
key2 = xor(to_blocks(ctxt))
return str(base64.b64encode(iv+ctxt), encoding='utf8')
def enc_plaintext():
print('Plaintext: ', end='')
txt = base64.b64decode(input().rstrip())
print(encrypt(txt, key1, iv1))
def enc_flag() :
print(encrypt(flag, key2, iv2))
Most of the logic there doesn't matter in the end. The only thing that matters is the following two facts:
After spending some time reading the code these two points became apparent, and then it was simply a matter of getting the encrypted flag twice in a row and using the first encrypted flag as the key to decrypt the second one. Most of the time ended up being implementing a decrypt, as you can see in the following code:
from binascii import hexlify
BUFF = 256
BLOCK_SIZE = 16
encryptedFlag1 = 'TkK4jOlnRUoKpMQ3pbgea6mJ0ehf2jFNQajWBYOec0YitllbfcHaCak9IypFh47LubL+7EBGCI1VsOgGlpNbOA=='
encryptedFlag2 = 'stl7e5mp9Fy9FOTTuw1oOaMnIoSJGUJ8BI7lpDjiXtDbVcf2K4xgKymwpwItQ7kVfbX/Ecp0+Zcz9B2gl352Kw=='
resultBytes1 = base64.b64decode(encryptedFlag1)
resultBytes2 = base64.b64decode(encryptedFlag2)
decodedIv1 = resultBytes1[:16]
decodedCtxt1 = resultBytes1[16:]
decodedIv2 = resultBytes2[:16]
decodedCtxt2 = resultBytes2[16:]
flagKey = xor(to_blocks(decodedCtxt1))
flagCipher = decodedCtxt2
flagIv = decodedIv2
aes = AES.new(flagKey, AES.MODE_ECB)
bs = len(flagKey)
flagCipherBlocks = to_blocks(flagCipher)
decrypted = b''
curr = flagIv
for block in flagCipherBlocks :
d = xor(aes.decrypt(block), curr)
decrypted += d
curr = xor(decrypted[-bs:], block)
print(decrypted)