Inainte de laborator: Video Laborator 6

Laboratorul 06 - AES Byte at a Time ECB Decryption

Prezentarea PowerPoint pentru acest laborator poate fi găsită aici.

utils.py
import base64
 
# CONVERSION FUNCTIONS
def _chunks(string, chunk_size):
    for i in range(0, len(string), chunk_size):
        yield string[i:i+chunk_size]
 
def byte_2_bin(bval):
    """
      Transform a byte (8-bit) value into a bitstring
    """
    return bin(bval)[2:].zfill(8)
 
def _hex(x):
    return format(x, '02x')
 
def hex_2_bin(data):
    return ''.join(f'{int(x, 16):08b}' for x in _chunks(data, 2))
 
def str_2_bin(data):
    return ''.join(f'{ord(c):08b}' for c in data)
 
def bin_2_hex(data):
    return ''.join(f'{int(b, 2):02x}' for b in _chunks(data, 8))
 
def str_2_hex(data):
    return ''.join(f'{ord(c):02x}' for c in data)
 
def bin_2_str(data):
    return ''.join(chr(int(b, 2)) for b in _chunks(data, 8))
 
def hex_2_str(data):
    return ''.join(chr(int(x, 16)) for x in _chunks(data, 2))
 
# XOR FUNCTIONS
def strxor(a, b):  # xor two strings, trims the longer input
    return ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b))
 
def bitxor(a, b):  # xor two bit-strings, trims the longer input
    return ''.join(str(int(x) ^ int(y)) for (x, y) in zip(a, b))
 
def hexxor(a, b):  # xor two hex-strings, trims the longer input
    return ''.join(_hex(int(x, 16) ^ int(y, 16)) for (x, y) in zip(_chunks(a, 2), _chunks(b, 2)))
 
# BASE64 FUNCTIONS
def b64decode(data):
    return bytes_to_string(base64.b64decode(string_to_bytes(data)))
 
def b64encode(data):
    return bytes_to_string(base64.b64encode(string_to_bytes(data)))
 
# PYTHON3 'BYTES' FUNCTIONS
def bytes_to_string(bytes_data):
    return bytes_data.decode()  # default utf-8
 
def string_to_bytes(string_data):
    return string_data.encode()  # default utf-8

AES ECB

Cel mai simplu mod de criptare este ECB (Electronic Codebook). Mesajul este împărțit în blocuri, fiecare bloc fiind criptat separat.

CBC encryption CBC decryption

Ținând cont că fiecare bloc din mesaj este criptat individual cu cheia k, blocuri identice din mesaj vor rezulta în blocuri criptate identic. Putem astfel, în cazul în care folosim modul ECB, să ne așteptăm la multe porțiuni criptate repetate.

Această vulnerabilitate apare când:

  1. Trimitem un INPUT către server
  2. Serverul concatenează un mesaj secret INPUT → INPUT||secret
  3. Serverul criptează mesajul trimis folosind propria cheie → AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key)
  4. Serverul trimite rezultatul criptat înapoi către noi.

PREFIX poate fi un header de pachet sau orice altă informație “inutilă”.

Pentru următoarele exerciții vom folosi scheletul de laborator.

Exercițiul 1 - Determinarea dimensiunii blocului

Primul pas necesar, într-un atac asupra unei criptări pe blocuri, este determinarea dimensiunii unui bloc. Deși cunoaștem deja dimensiunea în acest caz, este un pas necesar în alte situații.

Pentru a afla dimensiunea, este suficient să trimitem, pe rând, mesaje din ce în ce mai mari (“A”, “AA”, “AAA”, …).

Funcția findBlockSize() mărește numărul de caractere adăugate, mărind lungimea mesajului.

Cum putem asocia dimensiunea mesajului cu numărul de blocuri criptate?

Exercițiul 2 - Determinarea dimensiunii prefixului

Vom aborda această problemă la fel ca în pasul anterior. Trimitem mesaje din ce în ce mai mari. Atunci când blocul criptat corespunzător prefixului nu se va mai schimba, putem calcula lungimea sa.

RRTT TT
RRXT TTT
RRXX TTTT
RRXX XTTT T  *detected that first block did not change*
RRXT TTT

În exemplul anterior am notat cu R caracterele prefixului, X = caracterele mesajului input și T mesajul target (secretul pe care dorim să îl aflăm).

Știm că avem lungimea (prefix + pad - 1) % block_size = 0

Ce se întâmplă dacă lungimea prefixului este mai mare decât lungimea unui singur bloc?

Exercițiul 3 - ECB Byte at a Time Attack

Presupunem că avem un algoritm de criptare-bloc care criptează 16 bytes, producând ciphertext de 16 bytes. Folosim acest algoritm pentru a cripta 2 blocuri de date necunoscute, m1 și m2. În plus, avem voie să trimitem propriul input m0, care va fi lipit în fața acestor blocuri. Pentru că putem trimite orice mesaj, vom alege să trimitem unul de lungime 16. Astfel, în cazul în care se folosește modul ECB, putem afla Enc(m0). Având acces la perechi de input propriu - ciphertext poate fi foarte util în acest caz, având de fapt un oracol. În cazul în care am trimite doar 15 bytes, putem afla ultimul byte prin brute force.

 Block 1          Block 2  Block 3
|RRXXXXXXXXXXXXX?|?......?|?......?|
 |----known----||--m1---|

Încercând toate cele 256 variante posibile pentru Block 1, putem asocia encripția corectă pentru un byte, folosind oracolul. Presupunem că am găsit byte-ul “w”. Repetăm același proces pentru următorul byte.

 Block 1          Block 2  Block 3
|RRXXXXXXXXXXXXw?|?......?|?......?|
 |----known----|

Repetând același proces, vom afla, rând pe rând, fiecare byte din secretul m1, găsind mesajul “we attack at daw”. Din păcate, în acest punct nu putem ajunge la mesajul m2. Dacă am alege m0 de lungime 0, obținem:

 M1               M2
|we attack at daw|?......?|
 |----known-----|

Totuși ne putem folosi de m1 pentru a continua atacul. Dacă alegem m0 de lungime 15 bytes, vom avea următoarea situatie:

 Block 1          Block 2          Block 3
|RRXXXXXXXXXXXXXw|e attack at daw?|?......?|
 |------------known-------------|

Se poate observa că putem să repetăm acum fix același proces pentru a afla byte-ul necunoscut din Block 2. Cât timp avem un oracol care să valideze atacul brute force, putem repeta procesul pentru a afla oricâți bytes din secret.

Lab Code

lab06.py
from math import ceil
import base64
import os
from random import randint
from Crypto.Cipher import AES
from utils import *
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()
 
 
def split_bytes_in_blocks(x, block_size):
    nb_blocks = ceil(len(x)/block_size)
    return [x[block_size*i:block_size*(i+1)] for i in range(nb_blocks)]
 
 
def pkcs7_padding(message, block_size):
    padding_length = block_size - (len(message) % block_size)
    if padding_length == 0:
        padding_length = block_size
    padding = bytes([padding_length]) * padding_length
    return message + padding
 
 
def pkcs7_strip(data):
    padding_length = data[-1]
    return data[:- padding_length]
 
 
def encrypt_aes_128_ecb(msg, key):
    padded_msg = pkcs7_padding(msg, block_size=16)
    cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
    encryptor = cipher.encryptor()
    return encryptor.update(padded_msg) + encryptor.finalize()
 
 
def decrypt_aes_128_ecb(ctxt, key):
    cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
    decryptor = cipher.decryptor()
    decrypted_data = decryptor.update(ctxt) + decryptor.finalize()
    message = pkcs7_strip(decrypted_data)
    return message
 
# You are not suppose to see this
class Oracle:
    def __init__(self):
        self.key = 'Mambo NumberFive'.encode()
        self.prefix = 'PREF'.encode()
        self.target = base64.b64decode(  # You are suppose to break this
            "RG8gbm90IGxheSB1cCBmb3IgeW91cnNlbHZlcyB0cmVhc3VyZXMgb24gZWFydGgsIHdoZXJlIG1vdGggYW5kIHJ1c3QgZGVzdHJveSBhbmQgd2hlcmUgdGhpZXZlcyBicmVhayBpbiBhbmQgc3RlYWwsCmJ1dCBsYXkgdXAgZm9yIHlvdXJzZWx2ZXMgdHJlYXN1cmVzIGluIGhlYXZlbiwgd2hlcmUgbmVpdGhlciBtb3RoIG5vciBydXN0IGRlc3Ryb3lzIGFuZCB3aGVyZSB0aGlldmVzIGRvIG5vdCBicmVhayBpbiBhbmQgc3RlYWwuCkZvciB3aGVyZSB5b3VyIHRyZWFzdXJlIGlzLCB0aGVyZSB5b3VyIGhlYXJ0IHdpbGwgYmUgYWxzby4="
        )
 
    def encrypt(self, message):
        return encrypt_aes_128_ecb(
            self.prefix + message + self.target,
            self.key
        )
 
# Task 1
def findBlockSize():
    initialLength = len(Oracle().encrypt(b''))
    i = 0
    while 1:  # Feed identical bytes of your-string to the function 1 at a time until you get the block length
        # You will also need to determine here the size of fixed prefix + target + pad
        # And the minimum size of the plaintext to make a new block
        length = len(Oracle().encrypt(b'X'*i))
        i += 1
 
        return block_size, sizeOfTheFixedPrefixPlusTarget, minimumSizeToAlighPlaintext
 
# Task 2
def findPrefixSize(block_size):
    previous_blocks = None
    # Find the situation where prefix_size + padding_size - 1 = block_size
    # Use split_bytes_in_blocks to get blocks of size(block_size)
 
    return prefix_size
 
 
# Task 3
def recoverOneByteAtATime(block_size, prefix_size, target_size):
    known_target_bytes = b""
    for _ in range(target_size):
        # prefix_size + padding_length + known_len + 1 = 0 mod block_size
        known_len = len(know_target_bytes)
 
        padding_length = (- known_len - 1 - prefix_size) % block_size
        padding = b"X" * padding_length
 
        # target block plaintext contains only known characters except its last character
        # Don't forget to use split_bytes_in_blocks to get the correct block
 
        # trying every possibility for the last character
 
    print(known_target_bytes.decode())
 
 
# Find block size, prefix size, and length of plaintext size to allign blocks
block_size, sizeOfTheFixedPrefixPlusTarget, minimumSizeToAlignPlaintext = findBlockSize()
print("Block size:\t\t\t" + str(block_size))
print("Size of prefix and target:\t" + str(sizeOfTheFixedPrefixPlusTarget))
print("Pad needed to align:\t\t" + str(minimumSizeToAlignPlaintext))
 
# Find size of the prefix
prefix_size = findPrefixSize(block_size)
print("\nPrefix Size:\t" + str(prefix_size))
 
# Size of the target
target_size = sizeOfTheFixedPrefixPlusTarget - \
    minimumSizeToAlignPlaintext - prefix_size
 
print("\nTarget:")
recoverOneByteAtATime(block_size, prefix_size, target_size)
ic/labs/06.txt · Last modified: 2021/11/07 22:18 by philip.dumitru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0