This shows you the differences between two versions of the page.
ic:labs:06 [2021/01/25 11:10] razvan.smadu |
ic:labs:06 [2023/10/09 23:22] (current) razvan.smadu |
||
---|---|---|---|
Line 3: | Line 3: | ||
===== Laboratorul 06 - AES Byte at a Time ECB Decryption ===== | ===== Laboratorul 06 - AES Byte at a Time ECB Decryption ===== | ||
- | The powerpoint presentation for this lab can be found [[https://drive.google.com/file/d/1YFkD5I6yNgVf1y9bhK4xdR2xZgDZ4Dd1/view?usp=sharing|here]]. | + | Prezentarea PowerPoint pentru acest laborator poate fi găsită [[https://drive.google.com/file/d/1YFkD5I6yNgVf1y9bhK4xdR2xZgDZ4Dd1/view?usp=sharing|aici]]. |
+ | Puteți lucra acest laborator folosind platforma Google Colab, accesând acest [[https://colab.research.google.com/github/ACS-IC-labs/IC-labs/blob/main/labs/lab06/lab6.ipynb|link]]. | ||
+ | |||
+ | |||
+ | <hidden> | ||
+ | <spoiler Click pentru a vedea utils.py> | ||
<file python utils.py> | <file python utils.py> | ||
import base64 | import base64 | ||
- | + | from typing import Generator | |
- | # 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): | + | |
- | """ | + | def _pad(data: str, size: int) -> str: |
- | Transform a byte (8-bit) value into a bitstring | + | reminder = len(data) % size |
+ | if reminder != 0: | ||
+ | data = "0" * (size - reminder) + data | ||
+ | return data | ||
+ | |||
+ | |||
+ | def _chunks(data: str, chunk_size: int) -> Generator[str, None, None]: | ||
+ | data = _pad(data, chunk_size) | ||
+ | for i in range(0, len(data), chunk_size): | ||
+ | yield data[i : i + chunk_size] | ||
+ | |||
+ | |||
+ | def _hex(data: int) -> str: | ||
+ | return format(data, "02x") | ||
+ | |||
+ | |||
+ | # Conversion functions | ||
+ | |||
+ | |||
+ | def byte_2_bin(bval: int) -> str: | ||
+ | """Converts a byte value to a binary string. | ||
+ | |||
+ | Args: | ||
+ | bval (int): | ||
+ | The byte value to be converted. It should be an integer between | ||
+ | 0 and 255. | ||
+ | |||
+ | Returns: | ||
+ | str: The binary string representation of the byte value, where each bit | ||
+ | is encoded as a character. The result has a fixed length of 8 characters | ||
+ | and is padded with leading zeros if necessary. | ||
+ | |||
+ | Examples: | ||
+ | >>> byte_2_bin(72) | ||
+ | '01001000' | ||
+ | >>> byte_2_bin(66) | ||
+ | '01000010' | ||
""" | """ | ||
return bin(bval)[2:].zfill(8) | return bin(bval)[2:].zfill(8) | ||
- | + | ||
- | def _hex(x): | + | |
- | return format(x, '02x') | + | def hex_2_bin(data: str) -> str: |
- | + | """Converts a hexadecimal string to a binary representation. | |
- | def hex_2_bin(data): | + | |
- | return ''.join(f'{int(x, 16):08b}' for x in _chunks(data, 2)) | + | Args: |
- | + | data (str): The hexadecimal string to be converted. It should have an | |
- | def str_2_bin(data): | + | even number of characters and only contain valid hexadecimal digits |
- | return ''.join(f'{ord(c):08b}' for c in data) | + | (0-9, A-F, a-f). |
- | + | ||
- | def bin_2_hex(data): | + | Returns: |
- | return ''.join(f'{int(b, 2):02x}' for b in _chunks(data, 8)) | + | str: The binary representation of the hexadecimal string, where each |
- | + | pair of hexadecimal digits is encoded as an 8-bit binary number. | |
- | def str_2_hex(data): | + | |
- | return ''.join(f'{ord(c):02x}' for c in data) | + | Examples: |
- | + | >>> hex_2_bin("01abcd") | |
- | def bin_2_str(data): | + | '000000011010101111001101' |
- | return ''.join(chr(int(b, 2)) for b in _chunks(data, 8)) | + | >>> hex_2_bin("0a") |
- | + | '00001010' | |
- | def hex_2_str(data): | + | """ |
- | return ''.join(chr(int(x, 16)) for x in _chunks(data, 2)) | + | return "".join(f"{int(x, 16):08b}" for x in _chunks(data, 2)) |
- | + | ||
- | # XOR FUNCTIONS | + | |
- | def strxor(a, b): # xor two strings, trims the longer input | + | def bin_2_hex(data: str) -> str: |
- | return ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)) | + | """Converts a binary string to a hexadecimal representation. |
- | + | ||
- | def bitxor(a, b): # xor two bit-strings, trims the longer input | + | Args: |
- | return ''.join(str(int(x) ^ int(y)) for (x, y) in zip(a, b)) | + | data (str): The binary string to be converted. It should have a multiple |
- | + | of 8 characters and only contain valid binary digits (0 or 1). | |
- | 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))) | + | Returns: |
- | + | str: The hexadecimal representation of the binary string, where each | |
- | # BASE64 FUNCTIONS | + | group of 8 binary digits is encoded as a pair of hexadecimal digits. |
- | def b64decode(data): | + | |
- | return bytes_to_string(base64.b64decode(string_to_bytes(data))) | + | Examples: |
- | + | >>> bin_2_hex("000000011010101111001101") | |
- | def b64encode(data): | + | '01abcd' |
+ | >>> bin_2_hex("00001010") | ||
+ | '0a' | ||
+ | """ | ||
+ | return "".join(f"{int(b, 2):02x}" for b in _chunks(data, 8)) | ||
+ | |||
+ | |||
+ | def str_2_bin(data: str) -> str: | ||
+ | """Converts a string to a binary representation. | ||
+ | |||
+ | Args: | ||
+ | data (str): The string to be converted. | ||
+ | |||
+ | Returns: | ||
+ | str: The binary representation of the string, where each character is | ||
+ | encoded as an 8-bit binary number. | ||
+ | |||
+ | Examples: | ||
+ | >>> str_2_bin("Hello") | ||
+ | '0100100001100101011011000110110001101111' | ||
+ | >>> str_2_bin("IC") | ||
+ | '0100100101000011' | ||
+ | """ | ||
+ | return "".join(f"{ord(c):08b}" for c in data) | ||
+ | |||
+ | |||
+ | def bin_2_str(data: str) -> str: | ||
+ | """Converts a binary string to a string. | ||
+ | |||
+ | Args: | ||
+ | data (str): The binary string to be converted. It should have a multiple | ||
+ | of 8 characters and only contain valid binary digits (0 or 1). | ||
+ | |||
+ | Returns: | ||
+ | str: The string representation of the binary string, where each group | ||
+ | of 8 binary digits is decoded as a character. | ||
+ | |||
+ | Examples: | ||
+ | >>> bin_2_str("0100100001100101011011000110110001101111") | ||
+ | 'Hello' | ||
+ | >>> bin_2_str("0100100101000011") | ||
+ | 'IC' | ||
+ | """ | ||
+ | return "".join(chr(int(b, 2)) for b in _chunks(data, 8)) | ||
+ | |||
+ | |||
+ | def str_2_hex(data: str) -> str: | ||
+ | """Converts a string to a hexadecimal representation. | ||
+ | |||
+ | Args: | ||
+ | data (str): The string to be converted. | ||
+ | |||
+ | Returns: | ||
+ | str: The hexadecimal representation of the string, where each character | ||
+ | is encoded as a pair of hexadecimal digits. | ||
+ | |||
+ | Examples: | ||
+ | >>> str_2_hex("Hello") | ||
+ | '48656c6c6f' | ||
+ | >>> str_2_hex("IC") | ||
+ | '4943' | ||
+ | """ | ||
+ | return "".join(f"{ord(c):02x}" for c in data) | ||
+ | |||
+ | |||
+ | def hex_2_str(data: str) -> str: | ||
+ | """Converts a hexadecimal string to a string. | ||
+ | |||
+ | Args: | ||
+ | data (str): The hexadecimal string to be converted. It should have an | ||
+ | even number of characters and only contain valid hexadecimal digits | ||
+ | (0-9, A-F, a-f). | ||
+ | |||
+ | Returns: | ||
+ | str: The string representation of the hexadecimal string, where each | ||
+ | pair of hexadecimal digits is decoded as a character. | ||
+ | |||
+ | Examples: | ||
+ | >>> hex_2_str("48656c6c6f") | ||
+ | 'Hello' | ||
+ | >>> hex_2_str("4943") | ||
+ | 'IC' | ||
+ | """ | ||
+ | return "".join(chr(int(x, 16)) for x in _chunks(data, 2)) | ||
+ | |||
+ | |||
+ | # XOR functions | ||
+ | |||
+ | |||
+ | def strxor(operand_1: str, operand_2: str) -> str: | ||
+ | """Performs a bitwise exclusive OR (XOR) operation on two strings. | ||
+ | |||
+ | Args: | ||
+ | operand_1 (str): The first string to be XORed. | ||
+ | operand_2 (str): The second string to be XORed. | ||
+ | |||
+ | Returns: | ||
+ | str: The result of the XOR operation on the two strings, where each | ||
+ | character is encoded as an 8-bit binary number. The result has | ||
+ | the same length as the shorter input string. | ||
+ | |||
+ | Examples: | ||
+ | >>> strxor("Hello", "IC") | ||
+ | '\\x01&' | ||
+ | >>> strxor("secret", "key") | ||
+ | '\\x18\\x00\\x1a' | ||
+ | """ | ||
+ | return "".join(chr(ord(x) ^ ord(y)) for (x, y) in zip(operand_1, operand_2)) | ||
+ | |||
+ | |||
+ | def bitxor(operand_1: str, operand_2: str) -> str: | ||
+ | """Performs a bitwise exclusive OR (XOR) operation on two bit-strings. | ||
+ | |||
+ | Args: | ||
+ | operand_1 (str): The first bit-string to be XORed. It should only | ||
+ | contain valid binary digits (0 or 1). | ||
+ | operand_2 (str): The second bit-string to be XORed. It should only | ||
+ | contain valid binary digits (0 or 1). | ||
+ | |||
+ | Returns: | ||
+ | str: The result of the XOR operation on the two bit-strings, where each | ||
+ | bit is encoded as a character. The result has the same length as | ||
+ | the shorter input bit-string. | ||
+ | |||
+ | Examples: | ||
+ | >>> bitxor("01001000", "01000010") | ||
+ | '00001010' | ||
+ | >>> bitxor("10101010", "00110011") | ||
+ | '10011001' | ||
+ | """ | ||
+ | return "".join(str(int(x) ^ int(y)) for (x, y) in zip(operand_1, operand_2)) | ||
+ | |||
+ | |||
+ | def hexxor(operand_1: str, operand_2: str) -> str: | ||
+ | """Performs a bitwise exclusive OR (XOR) operation on two hexadecimal | ||
+ | strings. | ||
+ | |||
+ | Args: | ||
+ | operand_1 (str): The first hexadecimal string to be XORed. It should | ||
+ | have an even number of characters and only contain valid hexadecimal | ||
+ | digits (0-9, A-F, a-f). | ||
+ | operand_2 (str): The second hexadecimal string to be XORed. It should | ||
+ | have an even number of characters and only contain valid | ||
+ | digits (0-9, A-F, a-f). | ||
+ | |||
+ | Returns: | ||
+ | str: The result of the XOR operation on the two hexadecimal strings, | ||
+ | where each pair of hexadecimal digits is encoded as a pair of | ||
+ | hexadecimal digits. The result has the same length as the shorter | ||
+ | input hexadecimal string. | ||
+ | |||
+ | Examples: | ||
+ | >>> hexxor("48656c6c6f", "42696e67") | ||
+ | '0a0c020b' | ||
+ | >>> hexxor("736563726574", "6b6579") | ||
+ | '18001a' | ||
+ | """ | ||
+ | return "".join( | ||
+ | _hex(int(x, 16) ^ int(y, 16)) | ||
+ | for (x, y) in zip(_chunks(operand_1, 2), _chunks(operand_2, 2)) | ||
+ | ) | ||
+ | |||
+ | |||
+ | # Python3 'bytes' functions | ||
+ | |||
+ | |||
+ | def bytes_to_string(bytes_data: bytearray | bytes) -> str: | ||
+ | """Converts a byte array or a byte string to a string. | ||
+ | |||
+ | Args: | ||
+ | bytes_data (bytearray | bytes): The byte array or the byte string to be | ||
+ | converted. It should be encoded in Latin-1 format. | ||
+ | |||
+ | Returns: | ||
+ | str: The string representation of the byte array or the byte string, | ||
+ | decoded using Latin-1 encoding. | ||
+ | |||
+ | Examples: | ||
+ | >>> bytes_to_string(b'Hello') | ||
+ | 'Hello' | ||
+ | >>> bytes_to_string(bytearray(b'IC')) | ||
+ | 'IC' | ||
+ | """ | ||
+ | return bytes_data.decode(encoding="raw_unicode_escape") | ||
+ | |||
+ | |||
+ | def string_to_bytes(string_data: str) -> bytes: | ||
+ | """Converts a string to a byte string. | ||
+ | |||
+ | Args: | ||
+ | string_data (str): The string to be converted. | ||
+ | |||
+ | Returns: | ||
+ | bytes: The byte string representation of the string, encoded using | ||
+ | Latin-1 encoding. | ||
+ | |||
+ | Examples: | ||
+ | >>> string_to_bytes('Hello') | ||
+ | b'Hello' | ||
+ | >>> string_to_bytes('IC') | ||
+ | b'IC' | ||
+ | """ | ||
+ | return string_data.encode(encoding="raw_unicode_escape") | ||
+ | |||
+ | |||
+ | # Base64 functions | ||
+ | |||
+ | |||
+ | def b64encode(data: str) -> str: | ||
+ | """Encodes a string to base64. | ||
+ | |||
+ | Parameters: | ||
+ | data (str): The string to be encoded. | ||
+ | |||
+ | Returns: | ||
+ | str: The base64 encoded string, using Latin-1 encoding. | ||
+ | |||
+ | Examples: | ||
+ | >>> b64encode("Hello") | ||
+ | 'SGVsbG8=' | ||
+ | >>> b64encode("IC") | ||
+ | 'SUM=' | ||
+ | """ | ||
return bytes_to_string(base64.b64encode(string_to_bytes(data))) | return bytes_to_string(base64.b64encode(string_to_bytes(data))) | ||
- | + | ||
- | # PYTHON3 'BYTES' FUNCTIONS | + | |
- | def bytes_to_string(bytes_data): | + | def b64decode(data: str) -> str: |
- | return bytes_data.decode() # default utf-8 | + | """Decodes a base64 encoded string. |
- | + | ||
- | def string_to_bytes(string_data): | + | Args: |
- | return string_data.encode() # default utf-8 | + | data (str): The base64 encoded string to be decoded. It should only |
+ | contain valid base64 characters (A-Z, a-z, 0-9, +, /, =). | ||
+ | |||
+ | Returns: | ||
+ | str: The decoded string, using Latin-1 encoding. | ||
+ | |||
+ | Examples: | ||
+ | >>> b64decode("SGVsbG8=") | ||
+ | 'Hello' | ||
+ | >>> b64decode("SUM=") | ||
+ | 'IC' | ||
+ | """ | ||
+ | return bytes_to_string(base64.b64decode(string_to_bytes(data))) | ||
</file> | </file> | ||
+ | </spoiler> | ||
==== AES ECB ==== | ==== AES ECB ==== | ||
- | The simplest of the encryption modes is the Electronic Codebook (ECB) mode (named after conventional physical codebooks). The message is divided into blocks, and each block is encrypted separately. | + | Cel mai simplu mod de criptare este ECB (Electronic Codebook). Mesajul este împărțit în blocuri, fiecare bloc fiind criptat separat. |
{{https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/ECB_encryption.svg/902px-ECB_encryption.svg.png?450|CBC encryption}} | {{https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/ECB_encryption.svg/902px-ECB_encryption.svg.png?450|CBC encryption}} | ||
{{https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/ECB_decryption.svg/902px-ECB_decryption.svg.png?450|CBC decryption}} | {{https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/ECB_decryption.svg/902px-ECB_decryption.svg.png?450|CBC decryption}} | ||
- | Since each block of plaintext is encrypted with the key independently, identical blocks of plaintext will yield identical blocks of ciphertext. | + | Ț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. |
- | Lots of people know that when you encrypt something in ECB mode, you can see penguins through it. | + | |
- | The vulnerability happens when: | + | Această vulnerabilitate apare când: |
- | - You send an INPUT to the server. | + | - Trimitem un INPUT către server |
- | - The server appends secret to INPUT -> INPUT||secret | + | - Serverul concatenează un mesaj secret INPUT -> INPUT||secret |
- | - The server encrypts it with a secret key and a prefix -> AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key) | + | - Serverul criptează mesajul trimis folosind propria cheie -> AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key) |
- | - The server returns | + | - Serverul trimite rezultatul criptat înapoi către noi. |
- | For the next exercises, we will use the following {{:ic:laboratoare:aesecb_Attack.zip|code stub}}. | + | PREFIX poate fi un header de pachet sau orice altă informație "inutilă". |
- | ==== Exercise 1 - Determining Block Size ==== | + | Pentru următoarele exerciții vom folosi {{:ic:laboratoare:aesecb_Attack.zip|scheletul de laborator}}. |
- | The first step in attacking a block-based cipher is to determine the size of the block. | + | |
- | Feed identical bytes of your-string to the function 1 at a time - start with 1 byte ("A"), then "AA", then "AAA" and so on. Discover the block size of the cipher. You know it, but do this step anyway. | + | ==== Exercițiul 1 - Determinarea dimensiunii blocului (3p) ==== |
+ | 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", ...). | ||
<note tip> | <note tip> | ||
- | Function **findBlockSize()** keeps on incrementing the padding length (and the message length also). | ||
- | How does the message length relates to the number of cypher blocks? | + | Funcția **find_block_size()** mărește numărul de caractere adăugate, mărind lungimea mesajului. |
+ | |||
+ | Cum putem asocia dimensiunea mesajului cu numărul de blocuri criptate? | ||
</note> | </note> | ||
- | ==== Exercise 2 - Determining Prefix size ==== | + | ==== Exercițiul 2 - Determinarea dimensiunii prefixului (3p) ==== |
- | We give some chosen plaintext of increasing length to the oracle. When we detect a block that does not change with the addition of one more byte of chosen plaintext, this means this block only contains prefix and chosen plaintext. Eg: | + | 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. |
<code> | <code> | ||
RRTT TT | RRTT TT | ||
Line 100: | Line 375: | ||
RRXT TTT | RRXT TTT | ||
</code> | </code> | ||
- | Using R to denote the random prefix, X for the input we would give to the oracle (hereafter called the chosen plaintext) and T for target. | ||
- | Now we know the pad length required to align the target to blocks. | + | Î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). |
<note tip> | <note tip> | ||
- | We also know that (prefix + pad - 1) % block_size = 0 | + | |
+ | Știm că avem lungimea (prefix + pad - 1) % block_size = 0 | ||
</note> | </note> | ||
<note important> | <note important> | ||
- | What happens if the prefix is longer than the block size? | + | Ce se întâmplă dacă lungimea prefixului este mai mare decât lungimea unui singur bloc? |
</note> | </note> | ||
- | ==== Exercise 3 - ECB Byte at a Time Attack ==== | + | ==== Exercițiul 3 - ECB Byte at a Time Attack (4p) ==== |
- | + | 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. | |
- | Suppose we have a block cipher that takes a 16 byte plaintext and produces a 16 byte ciphertext. We use this block cipher to encrypt two blocks worth of unknown data, call them m1 and m2. Additionally we are allowed to prepend some data to these two blocks, let's call it m0 (we control this data). | + | 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. |
- | Note that in this scheme nothing prevents us from choosing an m0 that is 16 bytes long. This means we effectively have an encryption oracle for a full block, since the first block returned in this case would be Enc(m0) if ECB mode is being used. This means we can get the encryption of arbitrary blocks of data, which will come in handy. | + | |
- | We can set m0 equal to 15 known bytes, and if we have an encryption oracle we can brute force the last byte: | + | |
<code> | <code> | ||
Block 1 Block 2 Block 3 | Block 1 Block 2 Block 3 | ||
Line 122: | Line 395: | ||
|----known----||--m1---| | |----known----||--m1---| | ||
</code> | </code> | ||
- | We just have to send all 256 possible guesses for Block 1 to the encryption oracle and see which one matches the output. Let's say we get a match on the byte encoding "w". We then repeat the process with a one byte shorter m0 to get the next byte in the same fashion: | + | Î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. |
<code> | <code> | ||
Block 1 Block 2 Block 3 | Block 1 Block 2 Block 3 | ||
Line 128: | Line 402: | ||
|----known----| | |----known----| | ||
</code> | </code> | ||
- | We can repeat this process for each byte until we have the whole first block m1, which let's say is "we attack at daw". Unfortunately at this point we can't reduce m0 by any more bytes since m0 would be 0 bytes and we would simply get: | + | 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: |
<code> | <code> | ||
M1 M2 | M1 M2 | ||
Line 134: | Line 408: | ||
|----known-----| | |----known-----| | ||
</code> | </code> | ||
- | But we since we now know all of m1 we can use the sort of attack we used to recover the first byte of m1 to recover the first byte of m2. Suppose we again choose m0 to be of length 15 bytes: | + | Totuși ne putem folosi de m1 pentru a continua atacul. Dacă alegem m0 de lungime 15 bytes, vom avea următoarea situatie: |
<code> | <code> | ||
Block 1 Block 2 Block 3 | Block 1 Block 2 Block 3 | ||
Line 140: | Line 414: | ||
|------------known-------------| | |------------known-------------| | ||
</code> | </code> | ||
- | There's only one unknown byte in Block 2 so all we have to do is again submit all 256 guesses to the encryption oracle, except this time for Block 2 instead of Block 1! This process can be repeated to decrypt an arbitrary amount of ciphertext that is ECB encrypted as long as we can prepend data to the plaintext and have access to an encryption oracle. | + | 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 ==== | ==== Lab Code ==== | ||
+ | <spoiler Click pentru a vedea lab06.py> | ||
<file python lab06.py> | <file python lab06.py> | ||
- | from math import ceil | ||
import base64 | import base64 | ||
- | import os | + | from math import ceil |
- | from random import randint | + | from typing import List, Tuple |
- | from Crypto.Cipher import AES | + | |
- | from utils import * | + | |
- | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | + | |
from cryptography.hazmat.backends import default_backend | from cryptography.hazmat.backends import default_backend | ||
+ | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | ||
+ | from utils import * | ||
+ | |||
backend = default_backend() | backend = default_backend() | ||
- | def split_bytes_in_blocks(x, block_size): | + | def split_bytes_in_blocks(x: bytes, block_size: int) -> List[bytes]: |
- | nb_blocks = ceil(len(x)/block_size) | + | """Splits a byte string into a list of blocks of equal size. |
- | return [x[block_size*i:block_size*(i+1)] for i in range(nb_blocks)] | + | |
+ | Args: | ||
+ | x (bytes): The byte string to split. | ||
+ | block_size (int): The size of each block in bytes. | ||
- | def pkcs7_padding(message, block_size): | + | Returns: |
+ | List[bytes]: A list of byte strings, each of length block_size, | ||
+ | except for the last one which may be shorter. | ||
+ | """ | ||
+ | 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: bytes, block_size: int) -> bytes: | ||
+ | """Applies PKCS#7 padding to a byte string. | ||
+ | |||
+ | Args: | ||
+ | message (bytes): The byte string to pad. | ||
+ | block_size (int): The size of the block in bytes. | ||
+ | |||
+ | Returns: | ||
+ | bytes: A byte string that is a multiple of block_size in length, | ||
+ | with padding bytes added at the end. The value of each padding | ||
+ | byte is equal to the number of padding bytes added. | ||
+ | """ | ||
padding_length = block_size - (len(message) % block_size) | padding_length = block_size - (len(message) % block_size) | ||
if padding_length == 0: | if padding_length == 0: | ||
Line 169: | Line 466: | ||
- | def pkcs7_strip(data): | + | def pkcs7_strip(data: bytes) -> bytes: |
+ | """Removes PKCS#7 padding from a byte string. | ||
+ | |||
+ | Args: | ||
+ | data (bytes): The byte string to strip. | ||
+ | |||
+ | Returns: | ||
+ | bytes: A byte string with the padding bytes removed from the end. | ||
+ | """ | ||
padding_length = data[-1] | padding_length = data[-1] | ||
- | return data[:- padding_length] | + | return data[:-padding_length] |
- | def encrypt_aes_128_ecb(msg, key): | + | def encrypt_aes_128_ecb(plaintext: bytes, key: bytes) -> bytes: |
- | padded_msg = pkcs7_padding(msg, block_size=16) | + | """Encrypts a byte string using AES-128 in ECB mode. |
+ | |||
+ | Args: | ||
+ | plaintext (bytes): The byte string to encrypt. It will be padded | ||
+ | using PKCS#7. | ||
+ | key (bytes): The encryption key. It must be 16 bytes in length. | ||
+ | |||
+ | Returns: | ||
+ | bytes: A byte string that is the encrypted version of plaintext. | ||
+ | """ | ||
+ | padded_msg = pkcs7_padding(plaintext, block_size=16) | ||
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | ||
encryptor = cipher.encryptor() | encryptor = cipher.encryptor() | ||
Line 181: | Line 496: | ||
- | def decrypt_aes_128_ecb(ctxt, key): | + | def decrypt_aes_128_ecb(ciphertext: bytes, key: bytes) -> bytes: |
+ | """Decrypts a byte string using AES-128 in ECB mode. | ||
+ | |||
+ | Args: | ||
+ | ciphertext (bytes): The byte string to decrypt. It must be a multiple of | ||
+ | 16 bytes in length. | ||
+ | key (bytes): The decryption key. It must be 16 bytes in length. | ||
+ | |||
+ | Returns: | ||
+ | bytes: A byte string that is the decrypted version of ciphertext. The | ||
+ | PKCS#7 padding will be removed. | ||
+ | """ | ||
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) | ||
decryptor = cipher.decryptor() | decryptor = cipher.decryptor() | ||
- | decrypted_data = decryptor.update(ctxt) + decryptor.finalize() | + | decrypted_data = decryptor.update(ciphertext) + decryptor.finalize() |
message = pkcs7_strip(decrypted_data) | message = pkcs7_strip(decrypted_data) | ||
return message | return message | ||
- | # You are not suppose to see this | + | |
class Oracle: | class Oracle: | ||
- | def __init__(self): | + | """A class that simulates an encryption oracle using AES-128 in ECB mode. |
- | self.key = 'Mambo NumberFive'.encode() | + | You are not suppose to see this""" |
- | self.prefix = 'PREF'.encode() | + | |
- | self.target = base64.b64decode( # You are suppose to break this | + | def __init__(self) -> None: |
- | "RG8gbm90IGxheSB1cCBmb3IgeW91cnNlbHZlcyB0cmVhc3VyZXMgb24gZWFydGgsIHdoZXJlIG1vdGggYW5kIHJ1c3QgZGVzdHJveSBhbmQgd2hlcmUgdGhpZXZlcyBicmVhayBpbiBhbmQgc3RlYWwsCmJ1dCBsYXkgdXAgZm9yIHlvdXJzZWx2ZXMgdHJlYXN1cmVzIGluIGhlYXZlbiwgd2hlcmUgbmVpdGhlciBtb3RoIG5vciBydXN0IGRlc3Ryb3lzIGFuZCB3aGVyZSB0aGlldmVzIGRvIG5vdCBicmVhayBpbiBhbmQgc3RlYWwuCkZvciB3aGVyZSB5b3VyIHRyZWFzdXJlIGlzLCB0aGVyZSB5b3VyIGhlYXJ0IHdpbGwgYmUgYWxzby4=" | + | self.key = "Mambo NumberFive".encode() |
+ | self.prefix = "PREF".encode() | ||
+ | |||
+ | # You are suppose to break this | ||
+ | self.target = base64.b64decode( | ||
+ | "RG8gbm90IGxheSB1cCBmb3IgeW91cnNlbHZlcyB0cmVhc3VyZXMgb24gZWFydGgsI" | ||
+ | "HdoZXJlIG1vdGggYW5kIHJ1c3QgZGVzdHJveSBhbmQgd2hlcmUgdGhpZXZlcyBicm" | ||
+ | "VhayBpbiBhbmQgc3RlYWwsCmJ1dCBsYXkgdXAgZm9yIHlvdXJzZWx2ZXMgdHJlYXN" | ||
+ | "1cmVzIGluIGhlYXZlbiwgd2hlcmUgbmVpdGhlciBtb3RoIG5vciBydXN0IGRlc3Ry" | ||
+ | "b3lzIGFuZCB3aGVyZSB0aGlldmVzIGRvIG5vdCBicmVhayBpbiBhbmQgc3RlYWwuC" | ||
+ | "kZvciB3aGVyZSB5b3VyIHRyZWFzdXJlIGlzLCB0aGVyZSB5b3VyIGhlYXJ0IHdpbG" | ||
+ | "wgYmUgYWxzby4=" | ||
) | ) | ||
- | def encrypt(self, message): | + | def encrypt(self, message: bytes) -> bytes: |
return encrypt_aes_128_ecb( | return encrypt_aes_128_ecb( | ||
self.prefix + message + self.target, | self.prefix + message + self.target, | ||
- | self.key | + | self.key, |
) | ) | ||
+ | |||
# Task 1 | # Task 1 | ||
- | def findBlockSize(): | + | def find_block_size() -> Tuple[int, int, int]: |
- | initialLength = len(Oracle().encrypt(b'')) | + | initial_length = len(Oracle().encrypt(b"")) |
i = 0 | 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 | + | block_size = 0 |
- | # And the minimum size of the plaintext to make a new block | + | size_of_prefix_target_padding = 0 |
- | length = len(Oracle().encrypt(b'X'*i)) | + | minimum_size_to_align_plaintext = 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 | i += 1 | ||
- | return block_size, sizeOfTheFixedPrefixPlusTarget, minimumSizeToAlighPlaintext | + | # TODO 1: find block_size, size_of_prefix_target_padding, |
+ | # and minimum_size_to_align_plaintext | ||
+ | break | ||
+ | |||
+ | return ( | ||
+ | block_size, | ||
+ | size_of_prefix_target_padding, | ||
+ | minimum_size_to_align_plaintext, | ||
+ | ) | ||
# Task 2 | # Task 2 | ||
- | def findPrefixSize(block_size): | + | def find_prefix_size(block_size: int) -> int: |
- | previous_blocks = None | + | initial_blocks = split_bytes_in_blocks(Oracle().encrypt(b""), block_size) |
- | # Find the situation where prefix_size + padding_size - 1 = block_size | + | |
- | # Use split_bytes_in_blocks to get blocks of size(block_size) | + | |
+ | # TODO 2: Find when prefix_size + padding_size - 1 = block_size | ||
+ | # Use split_bytes_in_blocks to get blocks of size block_size. | ||
+ | |||
+ | # TODO 2.1: Find the block containing the prefix by comparing | ||
+ | # initial_blocks and modified_blocks | ||
+ | # You may find enumerate() and zip() useful. | ||
+ | modified_blocks = split_bytes_in_blocks(Oracle().encrypt(b"X"), block_size) | ||
+ | prefix_block_index = 0 | ||
+ | |||
+ | # TODO 2.2: As now we know in which block to look, find when that block | ||
+ | # does not change anymore when adding more X's. The complementary will | ||
+ | # represent the prefix. | ||
+ | prefix_size_in_block = 0 | ||
+ | |||
+ | prefix_size = prefix_block_index * block_size + prefix_size_in_block | ||
return prefix_size | return prefix_size | ||
# Task 3 | # Task 3 | ||
- | def recoverOneByteAtATime(block_size, prefix_size, target_size): | + | def recover_one_byte_at_a_time( |
+ | block_size: int, | ||
+ | prefix_size: int, | ||
+ | target_size: int, | ||
+ | ) -> str: | ||
known_target_bytes = b"" | known_target_bytes = b"" | ||
+ | |||
for _ in range(target_size): | for _ in range(target_size): | ||
# prefix_size + padding_length + known_len + 1 = 0 mod block_size | # prefix_size + padding_length + known_len + 1 = 0 mod block_size | ||
- | known_len = len(know_target_bytes) | + | known_len = len(known_target_bytes) |
- | padding_length = (- known_len - 1 - prefix_size) % block_size | + | padding_length = (-known_len - 1 - prefix_size) % block_size |
padding = b"X" * padding_length | padding = b"X" * padding_length | ||
- | # target block plaintext contains only known characters except its last character | + | # TODO 3.1: Determine the target block index which contains only known |
- | # Don't forget to use split_bytes_in_blocks to get the correct block | + | # characters except its last character. |
+ | |||
+ | # TODO 3.2: Get the target block form split_bytes_in_blocks at the index | ||
+ | # previously determined. | ||
+ | |||
+ | # TODO 3.3: Try every possibility for the last character and search for | ||
+ | # the block that you already know. That character will be added to | ||
+ | # the known target bytes. | ||
+ | |||
+ | return known_target_bytes.decode() | ||
- | # trying every possibility for the last character | + | def main() -> None: |
+ | # Find block size, prefix size, and length of plaintext size to align blocks | ||
+ | ( | ||
+ | block_size, | ||
+ | size_of_prefix_target_padding, | ||
+ | minimum_size_to_align_plaintext, | ||
+ | ) = find_block_size() | ||
- | print(known_target_bytes.decode()) | + | print(f"Block size:\t\t\t\t{block_size}") |
+ | print( | ||
+ | "Size of prefix, target, and padding:" | ||
+ | f"\t{size_of_prefix_target_padding}" | ||
+ | ) | ||
+ | print(f"Pad needed to align:\t\t\t{minimum_size_to_align_plaintext}") | ||
+ | # Find size of the prefix | ||
+ | prefix_size = find_prefix_size(block_size) | ||
+ | print(f"\nPrefix Size:\t{prefix_size}") | ||
- | # Find block size, prefix size, and length of plaintext size to allign blocks | + | # Size of the target |
- | block_size, sizeOfTheFixedPrefixPlusTarget, minimumSizeToAlignPlaintext = findBlockSize() | + | target_size = ( |
- | print("Block size:\t\t\t" + str(block_size)) | + | size_of_prefix_target_padding |
- | print("Size of prefix and target:\t" + str(sizeOfTheFixedPrefixPlusTarget)) | + | - minimum_size_to_align_plaintext |
- | print("Pad needed to align:\t\t" + str(minimumSizeToAlignPlaintext)) | + | - prefix_size |
+ | ) | ||
- | # Find size of the prefix | + | # Recover the target |
- | prefix_size = findPrefixSize(block_size) | + | recovered_target = recover_one_byte_at_a_time( |
- | print("\nPrefix Size:\t" + str(prefix_size)) | + | block_size, |
+ | prefix_size, | ||
+ | target_size, | ||
+ | ) | ||
+ | print(f"\nTarget: {recovered_target}") | ||
- | # Size of the target | ||
- | target_size = sizeOfTheFixedPrefixPlusTarget - \ | ||
- | minimumSizeToAlignPlaintext - prefix_size | ||
- | print("\nTarget:") | + | if __name__ == "__main__": |
- | recoverOneByteAtATime(block_size, prefix_size, target_size) | + | main() |
</file> | </file> | ||
+ | </spoiler> | ||
+ | </hidden> |