Cryptopals zestaw 4 ćwiczenie 27

przez | 18 marca 2021

Tym razem nasze ćwiczenie ma za zadanie nauczyć nas dlaczego używanie klucza szyfrującego jako wektora inicjującego to bardzo zły pomysł. Do tego będziemy potrzebowali wyroczni używającej AES w trybie CBC.

W zawiązku z powyższym zaczynami od stworzenia odpowiedniej wyroczni. Założenia są następujące:

  1. Wyrocznia generuje losowy klucz szyfrujący od długości 16 bajtów czyli tyle samo co długość wektora inicjującego.
  2. Wyrocznia działa w trybie CBC.
  3. Wektor inicjujący ma taką samą wartość jak klucz szyfrujący.
  4. Jeżeli odszyfrowana wiadomość zawiera bajty o wartościach większych niż 0x7F generowany jest wyjątek.

W celu stworzenia tej wyroczni posłużymy się kodem wyroczni z ćwiczenia 16 z zestawu 2 (https://koltys.info/blog/2020/08/06/cryptopals-zestaw-2-cwiczenie-16/).

class OracleCbcKeyIsIV(object):
    M_ENCRYPTION_KEY = b''
    M_IV = b''
    
    def __init__(self):
        self.M_ENCRYPTION_KEY = generateRandomData(16)
        self.M_IV = self.M_ENCRYPTION_KEY
    
    def encryptData(self, user_data):
        user_data = user_data.replace(';', '')
        user_data = user_data.replace('=', '')
        plain_data = ("comment1=cooking%20MCs;userdata="+
                      user_data+
                      ";comment2=%20like%20a%20pound%20of%20bacon")
        plain_data = addPaddingPKCS7(bytearray(plain_data, 'ascii'), 16)
        cipher = AES.new(self.M_ENCRYPTION_KEY, AES.MODE_CBC, self.M_IV)
        return cipher.encrypt(plain_data)

    def isAdmin(self, ciphered_data):
        result = False
        cipher = AES.new(self.M_ENCRYPTION_KEY, AES.MODE_CBC, self.M_IV)
        plain_data = cipher.decrypt(ciphered_data)
        for val in plain_data:
            if val > 0x7f:
                print(b"Exception: >" + plain_data[:16])
                print(b"Exception: >" + plain_data[32:48])
                raise Exception(plain_data.hex())
        if b";admin=true;" in plain_data:
            result = True
        return result
    
    def getKey(self):
        return self.M_ENCRYPTION_KEY

Dodałem jeszcze metodę getKey dzięki której będzie można zweryfikować czy prawidłowo odgadliśmy wartość klucz szyfrującego/wektora inicjującego.

Sam atak jest banalnie prosty polega on na modyfikacji zaszyfrowanej wiadomości tak aby miała ona postać:
BLOK_1 + 16 bajtów o wartości 0x00 + BLOK_1. BLOK_1 to pierwsze 16 bajtów zaszyfrowanej wiadomośći. Po odszyfrowaniu tak spreparowanej wiadomości otrzymamy śmieci, aczkolwiek niekoniecznie. Przyjrzyjmy się bliżej wartości BLOK_1, ale zanim to zrobimy przypomnijmy sobie jak działa tryb CBC. Kiedy coś jest szyfrowane w tym trybie, każdy kolejny blok danych przed zaszyfrowaniem jest XORowany z wartością poprzedniego bloku po zaszyfrowaniu:

Ci = AES_CBC( Pi XOR Ci-1)

Gdzie Ci – wartość bloku po zaszyfrowaniu, Pi – wartość bloku przed zaszyfrowaniem, i-1 oznacza, że chodzi i poprzedni blok. Pierwszy blok danych jest wyjątkowy, ponieważ nie ma bloku który by go poprzedzał potrzebujemy wektora inicjującego, żeby wykonać operację XORowania przed zaszyfrowaniem.

Operacja odszyfrowywania przebiega podobnie, ale blok najpierw jest odszyfrowywany a następnie XORowany z zaszyfrowaną wartością poprzedniego bloku:

Pi = AES_CBC(Ci) XOR Ci-1

Oznaczenia są takie same jak w poprzednim wzorze, i podobnie jak tam, pierwszy blok jest wyjątkowy ponieważ potrzebuje on wektora inicjującego, żeby móc wykonać na nim operację XORowania po rozszyfrowaniu.

Kiedy nikt nie majstruje przy zaszyfrowanej wiadomości wynik odszyfrowywania wygląda następująco:
P1 = AES_CBC(C1) XOR IV (wektor inicjujący),
P2 = AES_CBC(C2) XOR C1,
P3 = AES_CBC(C3) XOR C2,
Natomiast kiedy zmodyfikujemy wiadomość przed jej rozszyfrowaniem tak aby miała postać: C1 + 16*0x00 + C1 to otrzymamy:
P’1 = AES_CBC(C1) XOR IV,
P’2 = Śmieci,
P’3 = AES_CBC(C1) XOR 16*0x00 = AES_CBC(C1).
Jeżeli teraz wykonamy operację XOR na P’1 i P’3 to otrzymamy IV:
P’1 XOR P’3 = AES_CBC(C1) XOR IV XOR AES_CBC(C1) = IV, ponieważ OPERACJA XOR na dwóch takich samych wartościach zawsze daje w wyniku wartość 0.

W ten sposób udało nam się odgadnąć wartość klucza szyfrującego oraz wektora inicjującego. Kod mojego rozwiązania poniżej:

def s4challenge27():
    print("Challenge 27")
    oracle = OracleCbcKeyIsIV()
    secret_data = oracle.encryptData("A"*32)
    secret_data = secret_data[:16]+bytearray([0]*16)+secret_data[:16]
    try:
        isAdmin = oracle.isAdmin(secret_data)
        if isAdmin:
            print("User is admin")
        else:
            print("User is not admin")
    except Exception as e:
        invalid_plain = bytes.fromhex(str(e))
        print("Data: >", invalid_plain)
        print("Data: >", invalid_plain[:16])
        print("Data: >", invalid_plain[32:48])
        enc_key = bytes_xor(invalid_plain[:16], invalid_plain[32:48])
        print("Recovered key: ", enc_key)
        print("Oracle key: ", oracle.getKey())

Całość jak zawsze dostępna w repozytorium: https://gitlab.com/akoltys/cryptopals .

Dodaj komentarz

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