Cryptopals Set 2

Block Crypto

SPOILER WARNING: Try the cryptopals crypto challenges first!

Continuing the cryptopals crypto challenges with Set 2. Don’t read this unless you’re absolutely sure you don’t want to try them out yourself.

Notes for previous sets:

In [1]:
from binascii import unhexlify, b2a_base64, a2b_base64, hexlify
from Crypto.Cipher import AES
import random
import string

Challenge 9: Implement PKCS#7 padding

For some reason we didn’t google this and just used the example on the page to guide us, leading to some headaches many challenges later. Turns out the ‘\x04’ thing isn’t just a random byte, it’s because there needs to be 4 bytes of padding. Go figure.

The wiki said something about padding a full block extra if the string is already an even multiple of the block-size so that the padding can be detected even if ‘\x01’ is the last byte of the actual string.

In [2]:
def pkcs7_padding(byte_string: bytes, block_length: int) -> bytes:
    num_to_pad = block_length - (len(byte_string) % block_length)
    return byte_string + bytes([num_to_pad]) * num_to_pad

Challenge 10: Implement CBC mode

This was so much fun! Everything we know is from these pictures from the wiki

CBC encryption CBC decryption

In [3]:
# Helper functions from the previous set

def fixed_xor(buffer1: bytes, buffer2: bytes) -> bytes:
    return bytes([(b1 ^ b2) for b1, b2 in zip(buffer1, buffer2)])
 
def bytes_to_str(byte_list: bytes) -> str:
    return "".join(filter(lambda x: x in string.printable, "".join(map(chr, byte_list))))

def aes_in_ecb_mode(byte_string: bytes, key: bytes, encrypt: bool = False) -> bytes:
    cipher = AES.new(key, AES.MODE_ECB)
    if encrypt:
        return cipher.encrypt(byte_string)
    else:
        return cipher.decrypt(byte_string)
In [4]:
def cbc_mode(byte_string: bytes, 
             key: bytes, 
             initialization_vector: bytes, 
             encrypt: bool = True) -> bytes:
    if encrypt: 
        previous_block = initialization_vector
        cipher_text = b''
        for i in range(0, len(byte_string), len(key)):
            plain_text = fixed_xor(pkcs7_padding(byte_string[i: i + len(key)], len(key)),
                                   previous_block)
            previous_block = aes_in_ecb_mode(plain_text, key, encrypt=True)
            cipher_text += previous_block
        return cipher_text
    else:
        previous_block = initialization_vector
        plain_text = b''
        for i in range(0, len(byte_string), len(key)):
            cipher_text = byte_string[i: i + len(key)]
            plain_text += fixed_xor(aes_in_ecb_mode(cipher_text, key, encrypt=False), previous_block)
            previous_block = cipher_text
        return plain_text
In [5]:
byte_string = b''.join([a2b_base64(line.strip()) for line in open("data/crypto/10.txt").readlines()])
for line in bytes_to_str(cbc_mode(byte_string, b'YELLOW SUBMARINE', b'\x00'*16, encrypt=False)).split("\n")[:10]:
    print(line)
I'm back and I'm ringin' the bell 
A rockin' on the mike while the fly girls yell 
In ecstasy in the back of me 
Well that's my DJ Deshay cuttin' all them Z's 
Hittin' hard and the girlies goin' crazy 
Vanilla's on the mike, man I'm not lazy. 

I'm lettin' my drug kick in 
It controls my mouth and I begin 
To just let it flow, let my concepts go 

I almost want to listen to this song now.

Challenge 11: An ECB/CBC encryption oracle

Write a function that appends random stuff before and after your plaintext and then encrypts it with either CBC or ECB, who knows! (Well, we do since we wrote it but hey, not the point)

In [6]:
def generate_random_bytes(num_bytes: int) -> bytes:
    return bytes([random.randint(0, 255) for _ in range(num_bytes)])

MODE_ECB = 0
MODE_CBC = 1
BLOCK_SIZE = 16

def encryption_oracle(byte_string: bytes) -> tuple:
    append_before = generate_random_bytes(random.randint(5, 10))
    append_after = generate_random_bytes(random.randint(5, 10))
    plain_text = pkcs7_padding(append_before + byte_string + append_after, BLOCK_SIZE)
    random_key = generate_random_bytes(BLOCK_SIZE)
    method = random.randint(0, 1)
    if method == MODE_ECB:
        return aes_in_ecb_mode(plain_text, random_key, encrypt=True), MODE_ECB
    else:
        initialization_vector = generate_random_bytes(BLOCK_SIZE)
        return cbc_mode(plain_text, random_key, initialization_vector, encrypt=True), MODE_CBC

Now we’ve to detect which mode the function is currently using assuming it’s totally closed-source and all you can do is change the input and look at the output. (This took me a while because it says block box in the question and I thought that meant you only get access to the final encrypted string, not the black box). Anyway, we have code for this from Challenge 8 (in Set 1)

In [7]:
# from Set 1
def detect_aes_in_ecb_mode(byte_string: bytes,
                           block_length: int) -> bool:
    byte_blocks = [byte_string[i*block_length: (i+1)*block_length]
                   for i in range(int(len(byte_string) / block_length))]
    unique_blocks = set(byte_blocks)
    return len(unique_blocks)/len(byte_blocks) < 1
In [8]:
def decryption_oracle(encryptor) -> bool:
    plain_text = b'a' * (BLOCK_SIZE * 5)
    encrypted_string, mode = encryptor(plain_text)
    if detect_aes_in_ecb_mode(encrypted_string, BLOCK_SIZE):
        predicted_mode = MODE_ECB
    else:
        predicted_mode = MODE_CBC
    return predicted_mode == mode
In [9]:
count = 0
for i in range(1000):
    if decryption_oracle(encryption_oracle):
        count += 1
count
Out[9]:
1000

Challenge 12: Byte-at-a-time ECB decryption (Simple)

Here be dragons. Find the string an ECB encrypting function appends to yours before it does the encryption, given the function as a black box.

First off, here’s our black box:

In [10]:
GLOBAL_KEY = generate_random_bytes(BLOCK_SIZE)
UNKNOWN_STRING = b'Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg' \
                 b'aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq' \
                 b'dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg' \
                 b'YnkK'


def ecb_encryption_oracle(byte_string: bytes)->bytes:
    unknown_string = a2b_base64(UNKNOWN_STRING)
    plain_text = byte_string + unknown_string
    return aes_in_ecb_mode(pkcs7_padding(plain_text, BLOCK_SIZE), GLOBAL_KEY, encrypt=True)

First we find the block size and the number of blocks the unkown string takes after padding.

In [11]:
for i in range(256):
    if i == 0:
        unknown_string_length = len(ecb_encryption_oracle(b'A'*i))
        length_output = unknown_string_length
        continue
    new_length_output = len(ecb_encryption_oracle(b'A'*i))
    block_size = new_length_output - length_output
    if block_size != 0:
        break
    length_output = new_length_output

Let’s start with the first block of 16 bytes. We’ll make it 15 bytes of A. Then the last byte of this block becomes the first byte of the unknown string. We find this character by making a dict of the encrypted block for all possible last-byte characters. and checking it with the encrypted block of our 15 byte thing.

In [12]:
input_block = b'A'*15
last_byte_dict = {ecb_encryption_oracle(input_block + bytes([b]))[:16]: bytes([b]) for b in range(256)}
one_byte_short = ecb_encryption_oracle(b'A'*15)[:16]
first_unknown_byte = last_byte_dict[one_byte_short]

For the second byte of the unknown string, we make a block of 14 bytes of A, encrypt it, and check against the encrypted version of 14 A + first unknown byte + [all possible characters]. Once we know this we can do the third byte, and so on.

In [13]:
unknown_string_block = b''
input_block=b'A'*block_size # block size = 16
for n in range(block_size-1, -1, -1):
    print(input_block)
    input_block = input_block[1:] 
    last_byte_dict = {ecb_encryption_oracle(input_block + bytes([b]))[:block_size]: bytes([b]) for b in range(256)}
    one_byte_short = ecb_encryption_oracle(b'A'*n)[:block_size]
    last_byte = last_byte_dict[one_byte_short]
    unknown_string_block += last_byte
    input_block += last_byte
b'AAAAAAAAAAAAAAAA'
b'AAAAAAAAAAAAAAAR'
b'AAAAAAAAAAAAAARo'
b'AAAAAAAAAAAAARol'
b'AAAAAAAAAAAARoll'
b'AAAAAAAAAAARolli'
b'AAAAAAAAAARollin'
b"AAAAAAAAARollin'"
b"AAAAAAAARollin' "
b"AAAAAAARollin' i"
b"AAAAAARollin' in"
b"AAAAARollin' in "
b"AAAARollin' in m"
b"AAARollin' in my"
b"AARollin' in my "
b"ARollin' in my 5"

Looking good.

Okay so now we just extend this to the next blocks, until we run out of characters. The concept remains the same except we look at a different part of the one_byte_short string now and use the b’A’s to position that block the way we want it.

In [14]:
def byte_at_a_time_ecb_decryption_simple(encryptor)->bytes:
    for i in range(BLOCK_SIZE * 5): # This could be a while True if you really don't know the upper bound for your block size.
        if i == 0:
            unknown_string_length = len(encryptor(b'A'*i))
            length_output = unknown_string_length
            continue
        new_length_output = len(encryptor(b'A'*i))
        block_size = new_length_output - length_output
        if block_size != 0:
            break
        length_output = new_length_output
        
    if not detect_aes_in_ecb_mode(encryptor(b'A'*block_size*5), block_size):
        return b'Not ECB Mode'

    num_blocks = int(unknown_string_length/block_size)
    unknown_string = b''
    input_block = b'A'*block_size
    for block_number in range(num_blocks):
        unknown_string_block = b''
        for n in range(block_size-1, -1, -1):
            input_block = input_block[1:]
            last_byte_dict = {encryptor(input_block + bytes([b]))[:block_size]: bytes([b]) for b in range(256)}
            one_byte_short = encryptor(b'A'*n)[block_size*block_number: block_size*(block_number+1)]
            if one_byte_short not in last_byte_dict:
                break
            last_byte = last_byte_dict[one_byte_short]
            unknown_string_block += last_byte
            input_block += last_byte
        unknown_string += unknown_string_block
        input_block = unknown_string_block
    return unknown_string
In [15]:
for line in bytes_to_str(byte_at_a_time_ecb_decryption_simple(ecb_encryption_oracle)).split("\n"):
    print(line)
Rollin' in my 5.0
With my rag-top down so my hair can blow
The girlies on standby waving just to say hi
Did you stop? No, I just drove by

Challenge 13: ECB cut-and-paste

Make an encrypted admin profile.

The example user profile in the question is “email=foo@bar.com&uid=10&role=user”, so naturally I assumed the function increments uid each time it’s called. Googled around a bit, found a way to do it using a global variable USER_ID set as itertools.count() and having the function call next(USER_ID). Of course this probably isn’t what the actual question had in mind, since that makes breaking it through cut-and-paste pretty tough.

Anyway, here’s how we parsed cookies.

In [16]:
def cookie_parsing_routine(input_cookie: (str, dict), encode=True)->(dict, str):
    if encode:
        return "&".join(["=".join([key, input_cookie[key]]) for key in ["email", "uid", "role"]])
    else:
        return {p.split("=")[0].strip(): p.split("=")[1].strip() for p in input_cookie.split("&")}

And here’s the boring, fixed uid profile maker. I chose to remove reserved characters.

In [17]:
def profile_for(email_id: str):
    return cookie_parsing_routine({"email": email_id.replace("&", "").replace("=", ""),
                                   "uid": '10',
                                   "role": 'user'})

Encryption is just the normal ECB mode encryption.

In [18]:
def encrypt_user_profile(user_profile: bytes)->bytes:
    return aes_in_ecb_mode(pkcs7_padding(user_profile, BLOCK_SIZE), GLOBAL_KEY, encrypt=True)

Decryption is ECB mode + our cookie parser.

In [19]:
def decrypt_user_profile(encrypted_profile: bytes)->dict:
    return cookie_parsing_routine(bytes_to_str(aes_in_ecb_mode(encrypted_profile, GLOBAL_KEY, encrypt=False)), encode=False)

So that’s all our auxilliary functions done. Now how does one go about making an admin profile. We need to replace the encrypted version of role=user with an encrypted role=admin but these are different lengths so they won’t be encrypted the same way. So we just need to put them in different blocks of 16 bytes. role= can be in its own block and admin can be in one, with padding.

The fixed thing before role= is &uid=10&. Together they’re 13 bytes. We can control the remaining 3 using the email. We just need an email id such that email=[email_id_first_half] is a multiple of 16 bytes and [email_id_second_half] is 3 bytes. With that we have email=[email_id]&uid=10&role= perfectly encrypted.

Then to find how admin is encrypted if padded to 16 bytes, we just make an email id that has admin in a separate block with artifical padding.

In [20]:
def ecb_cut_and_paste(encryptor, profile_maker)->bytes:
    # admin in its own block
    admin_email = b'a'*(BLOCK_SIZE - len("email=")) + pkcs7_padding(b'admin', BLOCK_SIZE)
    # &role= ends block
    num_blocks = int((len("&uid=10") + len("email=") + len("&role="))/BLOCK_SIZE) + 1
    user_email = "a"*(num_blocks*BLOCK_SIZE - (len("&uid=10") + len("email=") + len("&role=")))
    if len(user_email) < 10:
        user_email += "a" * BLOCK_SIZE
    user_email = user_email[:-10] + "@gmail.com"
    admin_email_profile = encryptor(profile_maker(admin_email.decode()).encode("utf-8"))
    user_profile = encryptor(profile_maker(user_email).encode("utf-8"))
    admin_profile = user_profile[:num_blocks*BLOCK_SIZE] + admin_email_profile[BLOCK_SIZE:BLOCK_SIZE*2]
    return admin_profile
In [21]:
decrypt_user_profile(ecb_cut_and_paste(encrypt_user_profile, profile_for))
Out[21]:
{'email': 'aaa@gmail.com', 'uid': '10', 'role': 'admin'}

Challenge 14: Byte-at-a-time ECB decryption (Harder)

So now, in addition to adding an unknown string after your plaintext, the ECB encryptor also adds random bytes before your plaintext.

In [22]:
MAX_SIZE = BLOCK_SIZE * 10
RANDOM_BYTES = generate_random_bytes(random.randint(1, int(MAX_SIZE/2)))

def ecb_encryption_oracle_harder(byte_string: bytes)->bytes: 
    unknown_string = a2b_base64(UNKNOWN_STRING)
    plain_text = RANDOM_BYTES + byte_string + unknown_string
    return aes_in_ecb_mode(pkcs7_padding(plain_text, BLOCK_SIZE), GLOBAL_KEY, encrypt=True)

If we knew exactly how many bytes long the random thing is, we could just use the same idea as in Challenge 12, the only difference being we account for those extra bytes.

First we find out how many blocks the random string takes by finding the smallest number of blocks > the longest common prefix of two encrypted strings.

In [23]:
from os.path import commonprefix

def find_block_size(encryptor):
    length_output = len(encryptor(b'A'*0))
    for i in range(1, MAX_SIZE):
        new_length_output = len(encryptor(b'A'*i))
        block_size = new_length_output - length_output
        if block_size != 0:
            break
        length_output = new_length_output
    return length_output, block_size

def find_num_random_blocks(encryptor, length_output, block_size):
    common_prefix_length = len(commonprefix([encryptor(b''), encryptor(b'A')]))
    for num_random_blocks in range(int(length_output/block_size)):
        if common_prefix_length < block_size * num_random_blocks:
            break
    return num_random_blocks

Then, we see how many ‘A’s we have to add to achieve this num_random_blocks.

In [24]:
def find_string_lengths(encryptor, block_size, num_random_blocks):
    encrypted_strings =[]
    for i in range(block_size):
        encrypted_strings.append(encryptor(b'A'*i))
        random_string_length = len(commonprefix(encrypted_strings))
        unknown_string_length = len(encrypted_strings[0]) - random_string_length
        if len(encrypted_strings) > 1 and random_string_length >= block_size * num_random_blocks and random_string_length % block_size == 0:
            min_addition = i-1
            break
        encrypted_strings = [encrypted_strings[-1]]
    return min_addition, random_string_length, unknown_string_length

This is possibly more convoluted than it needs to be, I’m not sure.

Now we just add this min_addition when making our every possible last byte dict. And, when finding the part of the encrypted string to look at, we offset by random_string_length.

In [25]:
def byte_at_a_time_ecb_decryption_harder(encryptor)->bytes:
    length_output, block_size = find_block_size(encryptor)
    num_random_blocks = find_num_random_blocks(encryptor, length_output, block_size)
    min_addition, random_string_length, unknown_string_length = find_string_lengths(encryptor, block_size, num_random_blocks)

    if not detect_aes_in_ecb_mode(encryptor(b'A'*MAX_SIZE), block_size):
        return b'Not ECB Mode'

    num_blocks = int(unknown_string_length/block_size)
    unknown_string = b''
    input_block = b'A'*(block_size + min_addition)
    for block_number in range(num_blocks):
        unknown_string_block = b''
        for n in range(min_addition + block_size - 1, min_addition - 1, -1):
            input_block = input_block[1:]
            last_byte_dict = {encryptor(input_block + bytes([b]))[random_string_length: random_string_length + block_size]: bytes([b]) for b in range(256)}
            offset = (block_size * block_number) + random_string_length
            one_byte_short = encryptor(b'A'*n)[offset: offset + block_size]
            if one_byte_short not in last_byte_dict:
                break
            last_byte = last_byte_dict[one_byte_short]
            unknown_string_block += last_byte
            input_block += last_byte
        unknown_string += unknown_string_block
        input_block = b'A'*min_addition + unknown_string_block
    return unknown_string
In [26]:
for line in bytes_to_str(byte_at_a_time_ecb_decryption_harder(ecb_encryption_oracle_harder)).split("\n"):
    print(line)
Rollin' in my 5.0
With my rag-top down so my hair can blow
The girlies on standby waving just to say hi
Did you stop? No, I just drove by

Challenge 15: PKCS#7 padding validation

This is the point where we realized our initial padding function (which just appended a random byte) was wrong, went back and fixed it, and then spent a while fixing all the challenges after it. Anyway, we take the last byte and check that all the bytes from the back, upto the number the last byte represents, are all that same last byte

In [27]:
byte_string = b'ICE ICE BABY\x01\x02\x03\x04'
last_byte = byte_string[-1]
for i in range(last_byte, 0, -1):
    if byte_string[-i] != last_byte:
        raise ValueError("bad padding")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-27-1e54fd0d7f4e> in <module>
      3 for i in range(last_byte, 0, -1):
      4     if byte_string[-i] != last_byte:
----> 5         raise ValueError("bad padding")

ValueError: bad padding
In [28]:
def pkcs7_padding_validation(byte_string: bytes)->bytes:
    last_byte = byte_string[-1]
    if last_byte > len(byte_string):
        return ValueError("bad padding")
    for i in range(last_byte, 0, -1):
        if byte_string[-i] != last_byte:
            raise ValueError("bad padding")
    return byte_string[:-last_byte]

Challenge 16: CBC bitflipping attacks

Make an admin account in CBC mode.

The encryptor adds a prefix and a suffix to our plaintext and then quotes out all ;s and =s, before encrypting in CBC mode. The checker decrypts the input and checks if ;admin=true; is in it. The task is manipulate an encrypted string such that it passes the checker.

Here’s our encryptor and checker functions.

In [29]:
RANDOM_INITIALIZATION_VECTOR = generate_random_bytes(BLOCK_SIZE)

def cbc_bitflipping(byte_string: bytes, encrypt=True)->bytes:
    if encrypt:
        prefix = b'comment1=cooking%20MCs;userdata='
        suffix = b';comment2=%20like%20a%20pound%20of%20bacon'
        input_string = (prefix + byte_string + suffix).replace(b';', b'";"').replace(b'=', b'"="')
        return cbc_mode(pkcs7_padding(input_string, BLOCK_SIZE), GLOBAL_KEY, RANDOM_INITIALIZATION_VECTOR, encrypt=True)
    else:
        return cbc_mode(byte_string, GLOBAL_KEY, RANDOM_INITIALIZATION_VECTOR, encrypt=False)

def cbc_bitflipping_check(byte_string:bytes)->bool:
    decrypted_string = cbc_bitflipping(byte_string, encrypt=False)
    if b';admin=true;' in decrypted_string:
        return True
    else:
        return False

The clue is that a bit error in CBC mode scrambles the block the error is in (on decryption) but transmits the error to the same position in the next block. The flowchart from the wiki comes in handy again:

To make a block of plaintext - the previous block’s ciphertext is XORed with the current block’s ECB decrypted ciphertext.

Say ciphertext_block_1 is “xxxx” (assuming block size 4 for now, even though AES needs minimum 16) and ciphertext_block_2 is “ghjk”. Then plaintext_block_2 = decrypt_ECB("ghjk") $\oplus$ “xxxx”.

If we change ciphertext_block_1 to “xxbx”, plaintext_block_2 becomes decrypt_ECB("ghjk") $\oplus$ “xxbx”, i.e only the 3rd position changes.

Suppose we know plaintext_block_2, it’s “bird” (somehow). Cacofonix would like it to be “bard”. He needs to find a byte “y” such that he can change ciphertext_block_1 to “xyxx” and get “bard” as plaintext_block_2.

decrypt_ECB("ghjk") $\oplus$ “xxxx” = “bird”
decrypt_ECB("ghjk") $\oplus$ “xyxx” = “bard”

Let’s say $h*$ is the ECB decrypted “h”. Also, the inverse of XOR is XOR.

$$h^* \oplus x = \text{“i”}$$$$h^* = x \oplus \text{“i”}$$$$h^* \oplus y = \text{“a”}$$$$y = h^* \oplus \text{“a”} = x \oplus \text{“i”} \oplus \text{“a”}$$

Back to our question, we can input “xadminxtruex” to our encryptor and then change the xs into ;s and =s. For this we first need to make sure our admin input is in a separate block, which we do by adding a bunch of As before it, so that all of the prefix is padded upto a multiple of the block size. Then we need to identify the block previous to the admin block. Then, at positions 0, 6, and 11 on this previous block (these are the positions of the xs in the admin block), we use our bitflipping formula from above to make them into “;”, “=”, and “;” respectively.

In [30]:
def cbc_bitflipping_attack(encryptor, checker):
    # Number of blocks taken up by the prefix:
    num_prefix_blocks = len(commonprefix([encryptor(b''), 
                                          encryptor(b'A')])) // BLOCK_SIZE + 1
    
    # Number of extra letters to add such that 
    # the prefix + extra takes up a whole number of blocks
    encrypted_strings = [encryptor(b'A'*0)]
    min_addition = None
    for i in range(1, BLOCK_SIZE):
        encrypted_strings.append(encryptor(b'A'*i))
        length_common_prefix = len(commonprefix(encrypted_strings))
        if length_common_prefix == num_prefix_blocks*BLOCK_SIZE:
            min_addition = i-1
            break
        encrypted_strings = [encrypted_strings[-1]]
    assert min_addition is not None
    
    encrypted = encryptor(b'A'*min_addition + b'xadminxtruex')
    previous_block = [p for p in encrypted[(num_prefix_blocks-1)*BLOCK_SIZE: num_prefix_blocks*BLOCK_SIZE]]
    previous_block[0] ^= ord(b'x') ^ ord(b';')
    previous_block[6] ^= ord(b'x') ^ ord(b'=')
    previous_block[11] ^= ord(b'x') ^ ord(b';')
    previous_block = bytes(previous_block)
    admin_string = encrypted[:(num_prefix_blocks-1)*BLOCK_SIZE] + previous_block + encrypted[num_prefix_blocks*BLOCK_SIZE:]
    return checker(admin_string)
In [31]:
cbc_bitflipping_attack(cbc_bitflipping, cbc_bitflipping_check)
Out[31]:
True

And that’s Set 2.

Here’s Set 3



For comments, click the arrow at the top right corner.