Я намучился с тем, чтобы режим счётчика (CTR) в PyCrypto работал так, как мне нужно, поэтому реализовал CTR сам поверх режима ECB. Мой счётчик может увеличиваться и уменьшаться, чтобы запутать криптоаналитиков! У них нет ни малейшего шанса прочитать моё изображение.
Исходный код
from Crypto.Cipher import AES
KEY = ?
class StepUpCounter(object):
def __init__(self, step_up=False):
self.value = os.urandom(16).hex()
self.step = 1
self.stup = step_up
def increment(self):
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
self.value = self.newIV[2:len(self.newIV)]
return bytes.fromhex(self.value.zfill(32))
def __repr__(self):
self.increment()
return self.value
@chal.route('/bean_counter/encrypt/')
def encrypt():
cipher = AES.new(KEY, AES.MODE_ECB)
ctr = StepUpCounter()
out = []
with open("challenge_files/bean_flag.png", 'rb') as f:
block = f.read(16)
while block:
keystream = cipher.encrypt(ctr.increment())
xored = [a^b for a, b in zip(block, keystream)]
out.append(bytes(xored).hex())
block = f.read(16)
return {"encrypted": ''.join(out)}
Режим счётчика (CTR)
CTR превращает блочный шифр в потоковый. Вместо шифрования самого открытого текста шифруется значение счётчика, а результат XOR‘ится с блоком данных:
keystream_i = AES_ECB(key, nonce || counter_i)
ciphertext_i = plaintext_i ^ keystream_i
Для каждого следующего блока счётчик инкрементируется, поэтому каждый блок получает уникальный keystream. Если счётчик не меняется — все блоки шифруются одной и той же гаммой (keystream), и шифр превращается в многоразовый одноразовый блокнот.

Атака
Для понимания того, что нам делать, нужно разобрать код. А именно реализацию StepUpCounter:
class StepUpCounter(object):
def __init__(self, step_up=False):
self.value = os.urandom(16).hex()
self.step = 1
self.stup = step_up
def increment(self):
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
self.value = self.newIV[2:len(self.newIV)]
return bytes.fromhex(self.value.zfill(32))
Тут генерируется iv. А далее для каждого последующего применения он должен изменяться. Для этого и реализован метод increment.
Но в нём допущена ошибка:
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
Если self.stup == True, то значение iv растёт. Но в противном случае значение не изменяется: int(iv) - 0. False равен 0. Счётчик, который не считает.
Рассмотрим шифрование:
def encrypt():
cipher = AES.new(KEY, AES.MODE_ECB)
ctr = StepUpCounter()
out = []
with open("challenge_files/bean_flag.png", 'rb') as f:
block = f.read(16)
while block:
keystream = cipher.encrypt(ctr.increment())
xored = [a^b for a, b in zip(block, keystream)]
out.append(bytes(xored).hex())
block = f.read(16)
return {"encrypted": ''.join(out)}
Для получения гаммы в нём шифруется iv (в режиме ECB), а далее происходит XOR с открытым текстом. Но вот незадача. iv не изменяется, а значит каждый блок файла шифруется одной гаммой. Вот бы знать часть открытого текста…
А мы и знаем часть открытого текста. У форматов файлов часто есть единый формат заголовков. PNG — не исключение.
cu63:Downloads/ $ hexdump -C image.png | head
00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
Попробую взять эти 16 байт заголовка, сделаю XOR с первым блоком шифротекста (о свойствах XOR). А далее попробую расшифровать весь файл полученной гаммой.
Реализация
Как обычно, буду использовать Python для реализации:
import requests
URL = 'http://aes.cryptohack.org'
def encrypt():
url = f'{URL}/bean_counter/encrypt/'
response = requests.get(url)
if response.status_code == 200:
return response.json()['encrypted']
return ''
def xor(a: bytes, b: bytes) -> bytes:
return bytes([i ^ j for i, j in zip(a, b)])
def main():
raw = bytes.fromhex(encrypt())
png_header = bytes.fromhex('89504e470d0a1a0a0000000d49484452')
keystream = xor(png_header, raw[:16])
with open('flag.png', 'wb') as f:
for i in range(0, len(raw), 16):
f.write(xor(keystream, raw[i:i+16]))
if __name__ == '__main__':
main()
Запущу и открою картинку:

Флаг получен.