Cryptopals zestaw 2 ćwiczenie 12

przez | 6 lipca 2020

I tym razem będziemy wykorzystywali własność trybu ECB, ale teraz dostaniemy bardzo twardy dowód na to, że używanie AES w tym trybie to bardzo zły pomysł.

Na początek będziemy potrzebowali tzw. wyroczni, albo po angielsku Oracle. Wyrocznia to taki fajny i bardzo interesujący byt, posiada losowo wygenerowany klucz przy pomocy którego szyfrowane są dane przekazywane przez użytkownika. Na potrzeby tego zadania stworzyłem klasę OracleAesECB.

class OracleAesECB(object):
    M_ENCRYPTION_KEY = b''
    M_CIPHER = None
    M_TARGET = b''
    
    def __init__(self, payload):
        self.M_ENCRYPTION_KEY = generateRandomData(16)
        self.M_TARGET = bytes_from_base64(payload)
        
    def encrypt(self, input_data):
        self.M_CIPHER = AES.new(self.M_ENCRYPTION_KEY, AES.MODE_ECB)
        plain = input_data + self.M_TARGET
        plain = addPaddingPKCS7(plain, 16)
        return self.M_CIPHER.encrypt(plain)

Konstruktor klasy generuje losowy klucz o długości 16 bajtów i zapisuje go w zmiennej M_ENCRYPTION_KEY oraz odkodowuje z base64 sekret przekazany jako parametr i zapisuje go w zmiennej M_TARGET. M_TARGET to sekret, który będziemy musieli odzyskać. Oprócz konstruktora klasa ma jeszcze jedną metodę, której będziemy używali do odzyskania sekretu. Metoda do danych przekazanych przez użytkownika w zmiennej input_data dodaje sekret i wyrównuje długość całości, tak aby była wielokrotnością długości pojedyńczego bloku, dodając odpowiedni „padding” a następnie wszystko szyfruje AES w trybie ECB i zwraca zaszyfrowane dane użytkownikowi.

W kolejnym kroku stworzyłem funkcję, która wykrywa długość bloku używanego przez wyrocznię:

def detectOracleBlockSize(oracle):
    test_data = b''
    empty_size = len(oracle.encrypt(test_data))
    next_size = empty_size
    while empty_size == next_size:
        test_data = test_data + b'A'
        next_size = len(oracle.encrypt(test_data))
    return (next_size-empty_size)

Zasada działania tej funkcji jest prosta: przy do metody encrypt przekazujemy dane o długości 0 bajtów i zapamiętujemy długość zaszyfrowanych danych. W kolejnym kroku przekazujemy do metody encrypt dane o długości 1, 2, 3 itd. bajtów do momentu aż długość otrzymanych zaszyfrowanych danych będzie inna niż ta otrzymana na początku (kiedy przekazaliśmy dane o długości 0 bajtów). Różnica między nową i starą długością danych jest długością bloku używanego przez wyrocznię. To działa ponieważ używany jest padding, który wyrównuje dane tak aby były wielokrotnością długości jednego bloku danych. W związku z tym przekazując coraz to dłuższe dane do metody encrypt w pewnym momencie dochodzimy do momentu kiedy dane zwiększą się o kolejny blok.

Funkcję do wykrywania w jakim trybie działa dana wyrocznia omawiałem w poprzednim wpisie (https://koltys.info/blog/2020/06/30/cryptopals-zestaw-2-cwiczenie-11/).

Na tym etapie, wiemy już że wyrocznia używa AES w trybie ECB oraz znamy długość bloku. Dodatkowo wiemy, że do naszych danych dodawane są dane, których treść chcemy poznać. Okazuje się, że jest na to bardzo prosty sposób (oczywiście pod warunkiem, że mamy „wyrocznię”). Zgodnie z właściwością trybu ECB, która jest wałkowana już od kilku ćwiczeń:

Takie same bloki danych wejściowych zawsze produkują takie same bloki danych wyjściowych.

Jeżeli nasze dane będą krótsze o jeden bajt niż wynosi długość bloku danych to na końcu będzie znajdował pierwszy bajt sekretnej wiadomości. Jeżeli nasze dane będą krótsze o dwa bajty, na końcu będą dwa bajty sekretnej wiadomości itd. Poniżej przykład dla bloku danych o długości 8 bajtów.

D1D2D3D4D5D6D7S1S2S3Sn-1Sn
D1D2D3D4D5D6S1S2S3Sn-1SnX
D1D2D3D4D5S1S2S3Sn-1SnXX
Dx – bajty kontrolowane przez nas Sx – sekretne bajty.

Na początku przygotowujemy, dane krótsze o 1 bajt niż używana przez wyrocznię długość bloku i „prosimy” wyrocznię, żeby nam je zaszyfrowała. Wiemy, że w zaszyfrowanych danych pierwszy blok zawiera tylko jeden bajt którego wartości nie znamy. Zapamiętujemy sobie ten blok. Teraz „poprosimy” wyrocznię, żeby jeszcze raz coś nam zaszyfrowała, ale tym razem nasze dane będą miały długość równą długości bloku używanego przez wyrocznię. Jako ostatni bajt będziemy podawali po kolei wartości od 0 do 255 do momentu aż pierwszy blok zaszyfrowanych danych będzie miał taką samą wartość jak zapamiętany przez nas blok który uzyskaliśmy przy pierwszym wywołaniu np.:

  1. Wyrocznia ma długość bloku 16 bajtów i pracuje w trybie ECB.
  2. Do wyroczni za pierwszym razem przekazaliśmy wartość „AAAAAAAAAAAAAAA” (15 znaków 'A’) i dostaliśmy jakieś zaszyfrowane dane.
  3. W następnym kroku sprawdzamy co otrzymamy kiedy przekażemy do wyroczni wartość „AAAAAAAAAAAAAAA\x00” (15 znaków 'A’ i bajt o wartości 0x00). Jeżeli pierwszy blok zaszyfrowanych danych jest taki sam jak ten otrzymany w punkcie 1. oznacza to, że znamy pierwszą wartość sekretnej wiadomości, jeżeli nie próbujemy „AAAAAAAAAAAAAAA\x01”, „AAAAAAAAAAAAAAA\x02” itd. aż do „AAAAAAAAAAAAAAA\xff”.
  4. Znamy już pierwszy bajt sekretu – załóżmy, że ma on wartość 'S’, więc tym przekazujmy wyroczni wartość „AAAAAAAAAAAAAA” (14 znaków 'A’). i dostajemy jakieś zaszyfrowane dane. Teraz też, znamy wartości pierwszych piętnastu bajtów: „AAAAAAAAAAAAAAS” i znów musimy odgadnąć tylko wartość ostatniego bajtu. Postępujemy tak samo jak w punkcie 3. ale tym razem próbujemy wartości „AAAAAAAAAAAAAAS\x00”, „AAAAAAAAAAAAAAS\x01” itd. aż do „AAAAAAAAAAAAAAS\xff”.
  5. Teraz wiemy już, że drugi bajt ma wartość np. E. Powtarzamy tą procedurę do momentu aż odszyfrujemy cały pierwszy blok danych. Za każdym razem musimy sprawdzić w najgorszym przypadku 256 możliwości.
  6. Jeżeli sekretna wiadomości jest dłuższa niż jeden blok danych (najprawdopodobniej tak będzie) to po odszyfrowaniu pierwszego bloku, powtarzamy czynność od początku ale tym razem zapamiętujemy nie pierwszy a drug blok danych. Jeżeli podaliśmy wyroczni dane o długości 15 bajtów to w drugim bloku będzie 15 znanych nam bajtów i jeden bajt z kolejnego bloku. Cała procedura wygląda identycznie jak w pierwszym bloku danych, po prostu porównujemy drugie bloki zamiast pierwszych.
  7. Po odszyfrowaniu drugiego bloku znowu zaczynamy od początku ale tym razem zapamiętujemy trzeci blok danych itd. aż w końcu odszyfrujemy całą wiadomość.

Moja implementacja tego algorytmu wygląda następująco:

def s2challenge12():
    print("Challenge 12 start")
    secret_base64 = "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg" \
                    "aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq" \
                    "dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg" \
                    "YnkK"
    oracle = OracleAesECB(secret_base64)
    detected_block_size = detectOracleBlockSize(oracle)
    detected_ecb_mode = detectOracleECBMode(oracle)
    print("Secret size: ", len(secret_base64)*3/4)
    print("Detected block size: ", detected_block_size)
    print("Detected ECB mode: ", detected_ecb_mode)
    block_num = 0
    deciphered_message = []
    previous_block = [ord('A')]*(detected_block_size)
    decyphering_running = True
    while decyphering_running:
        deciphered_block = []
        pad_size = 1
        while pad_size <= detected_block_size:
            test_block = previous_block[pad_size:]
            ciphered_block = oracle.encrypt(bytearray(test_block))
            test_val = 0x0
            tmp_block = test_block+deciphered_block+[test_val]
            tmp_ciphered_block = oracle.encrypt(bytearray(tmp_block))
            block_offset = block_num*detected_block_size
            while tmp_ciphered_block[0:detected_block_size] != ciphered_block[block_offset:block_offset+detected_block_size] and test_val < 255:
                test_val = test_val + 1
                tmp_block = test_block+deciphered_block+[test_val]
                tmp_ciphered_block = oracle.encrypt(bytearray(tmp_block))
            deciphered_block += [test_val]
            pad_size += 1
        previous_block = deciphered_block
        deciphered_message += deciphered_block
        block_num += 1
        if block_num >= len(ciphered_block)/detected_block_size:
            decyphering_running = False
    print(bytearray(deciphered_message))

Cały kod można znaleźć na: https://gitlab.com/akoltys/cryptopals .

[AKTUALIZACJA]

Ten atak można przeprowadzić również na dane zaszyfrowane w trybie CBC. W tym wypadku fakt, że takie same bloki danych wejściowych produkują takie same bloki danych wyjściowych też ma tu zastosowanie ponieważ kontrolujemy dane znajdujące się przed sekretem, więc mamy wpływ na to z czym są XORowane następne bloki, więc sytuacja jest identyczna.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *