Example #1
0
def generate_hash_tree(hashlist, messages, md_hash, pb):
    # If there is only one hash in the list, we're done
    if len(hashlist[-1]) == 1: return hashlist, messages
    # Create pairs, initialise variables
    pairs = zip(hashlist[-1][::2], hashlist[-1][1::2])
    msg = set2.random_bytes(16)
    found_hashes, found_msgs = [], []

    # Iterate over all pairs we have
    for x, y in pairs:
        # See if we found a collision
        while md_hash(msg, x) != md_hash(msg, y):
            # If not, generate a new message
            msg = set2.random_bytes(16)
        pb.update(1)
        # Append found hash to the hash tree's current level
        found_hashes.append(md_hash(msg, x))
        # Append found message to the message tree's current level
        found_msgs.append(msg)

    # Add new level to hash tree and message tree
    hashlist.append(found_hashes)
    messages.append(found_msgs)
    # Go one level deeper
    return generate_hash_tree(hashlist, messages, md_hash, pb)
Example #2
0
def challenge_34():
    # Simple EchoBot exchange
    alice = EchoAlice()
    bob = EchoBob()

    msg1 = alice.initiate(p=nist_p, g=nist_g)
    msg2 = bob.receive(msg1)
    msg3 = alice.send_msg(msg2, set2.random_bytes(random.randrange(10, 100)))
    msg4 = bob.receive_msg(msg3)
    exchange_1_valid = alice.verify_echo(msg4)

    # EchoBot exchange with MitM
    alice = EchoAlice()
    bob = EchoBob()
    eve = EchoEve()

    secret_msg = set2.random_bytes(random.randrange(10, 100))
    msg1 = alice.initiate(p=nist_p, g=nist_g)
    msg1p = eve.intercept_alice_initiate(msg1)
    msg2 = bob.receive(msg1p)
    msg2p = eve.intercept_bob_receive(msg2)
    msg3 = alice.send_msg(msg2p, secret_msg)
    msg3p = eve.intercept_alice_send_msg(msg3)
    msg4 = bob.receive_msg(msg3p)
    msg4p = eve.intercept_bob_receive_msg(msg4)
    exchange_2_valid = alice.verify_echo(msg4p)
    exchange_2_cracked = secret_msg == eve.decrypt_message()

    # Verify exchanges were valid and Eve was able to obtain the original message
    assert_true(exchange_1_valid and exchange_2_valid and exchange_2_cracked)
Example #3
0
def expandable_message(k, md_hash):
    # Define the initial state
    initial_state = b'\x00' * HASH_LENGTH
    result = {}
    # Iterate from k to 0
    for i in range(k, 0, -1):
        # Generate a single message with a block length of 1
        single_msg = set2.random_bytes(16)
        single_msg_hash = md_hash(single_msg, initial_state)
        # Generate dummy blocks of length 2^(i-1)
        dummy_blocks = set2.random_bytes(16 * (2**(i - 1)))
        dummy_blocks_hash = md_hash(dummy_blocks, initial_state)

        poly_msg = None
        final_block = None
        # Bruteforce until we find a collision between `single_msg` and `dummy_blocks + final_block`
        while single_msg_hash != poly_msg:
            final_block = set2.random_bytes(16)
            poly_msg = md_hash(final_block, dummy_blocks_hash)
        # Verify the hashes are equal now
        assert md_hash(single_msg,
                       initial_state) == md_hash(dummy_blocks + final_block,
                                                 initial_state)
        # Append the shared hash, the short message and the long message to our result set
        result[i] = [single_msg_hash, single_msg, dummy_blocks + final_block]
        # Use the shared hash as the new initial state
        initial_state = single_msg_hash
    return result
Example #4
0
def challenge_28():
    # Generate random key, message
    key = set2.random_bytes(16)
    message = set2.random_bytes(random.randrange(128, 1024))
    # Compute mac for the generated key and message
    original_mac = simple_mac(key, message)
    # Verify changing a byte will change the mac significantly
    assert_true(mac_tamper(key, message, original_mac))
Example #5
0
def challenge_53():
    # Define our Merkle Damgard hash function
    md_hash = lambda x, y, z=False: MerkleDamgard(x, HASH_LENGTH, y, z)
    # Generate the message we'll attack
    M = set2.random_bytes(16 * 16)
    # Generate the hashmap, i.e. the intermediate hashes per block
    M_hashmap = md_hash(M, b'\x00' * HASH_LENGTH, True)
    # Compute `k`
    k = int(math.log2(len(M) // 16))
    # Generate expandable message
    print('Message to preimage has {} blocks (i.e. k = {})'.format(
        len(M) // 16, k))
    print("Generating expandable message... ", end='')
    expandable_output = expandable_message(k, md_hash)
    print('done')

    # Get hash of our final block in the expandable message
    final_hash = list(expandable_output.values())[-1][0]
    index = -1
    # Find an index that is at equal to or greater than `k`
    print("Generating bridge... ", end='')
    while index < k:
        bridge, bridge_hash = None, None
        # Check if generated message collides with one of our intermediate hashes
        while bridge_hash not in M_hashmap[1].keys():
            bridge = set2.random_bytes(16)
            bridge_hash = md_hash(bridge, final_hash)
        # If it does, that will be our index
        index = M_hashmap[1][bridge_hash]
    print("done\nFound collision against block {} of M".format(index))

    # The next step is to replace all blocks up until our collision (i.e. the bridge)
    # This requires some binary math, as sometimes the short message is required, while
    #  sometimes the long one is required.
    prefix_length = index
    prefix = b''
    for i in range(k, 0, -1):
        # Compute the length of the long message we're considering
        q = 2**(i - 1) + 1
        # If the prefix length minus the long message length is smaller than the remaining blocks:
        if prefix_length - q < i - 1:
            # Use short message
            prefix_length -= 1
            prefix += expandable_output[i][1]
        else:
            # Use long message
            prefix_length -= q
            prefix += expandable_output[i][2]
    # Construct the new message
    constructed_message = prefix + bridge + M[(16 * (index + 1)):]
    # Verify the constructed message has the same length and the same hash value as our original message
    assert_true(
        len(constructed_message) == len(M)
        and md_hash(constructed_message, b'\x00' * HASH_LENGTH) == md_hash(
            M, b'\x00' * HASH_LENGTH))
Example #6
0
def challenge_18():
    # Test given string
    test_string = "L77na/nrFsKvynd6HzOoG7GHTLXsTVu9qvY/2syLXzhPweyyMTJULu/6/kXX0KSvoOLSFQ=="
    assert_true(
        decrypt_aes_ctr(base64.b64decode(test_string), "YELLOW SUBMARINE", 0)
        == b'Yo, VIP Let\'s kick it Ice, Ice, baby Ice, Ice, baby ')

    # Generate random data, nonce and key, test if decrypting after encrypting results in the generated data again.
    data = set2.random_bytes(64)
    nonce = random.randint(128, 65536)
    key = set2.random_bytes(16)
    assert_true(
        data == decrypt_aes_ctr(encrypt_aes_ctr(data, key, nonce), key, nonce))
Example #7
0
 def send_msg(self, inp, msg):
     self.msg = msg
     # Generate the shared secret s
     self.s = modexp(inp, self.a, self.p)
     # Send plaintext using SHA1 hash of shared secret as key, generated IV as IV
     iv = set2.random_bytes(16)
     ciphertext = set2.encrypt_aes_cbc(key=hashlib.sha1(long_to_bytes(self.s)).digest()[0:16], iv=iv, text=msg)
     # Send ciphertext with IV
     return ciphertext, iv
Example #8
0
 def receive_msg(self, inp):
     (ciphertext, iv) = inp
     # Decrypt received message
     msg = set2.decrypt_aes_cbc(key=hashlib.sha1(long_to_bytes(self.s)).digest()[0:16], iv=iv, text=ciphertext)
     # Send plaintext using SHA1 hash of shared secret as key, generated IV as IV
     iv = set2.random_bytes(16)
     ciphertext = set2.encrypt_aes_cbc(key=hashlib.sha1(long_to_bytes(self.s)).digest()[0:16], iv=iv, text=msg)
     # Send ciphertext with IV
     return ciphertext, iv
Example #9
0
def challenge_24():
    # Initialise our new stream cipher
    stream_cipher_instance = MT19937Cipher(424242)
    # Generate a random plaintext
    plaintext = set2.random_bytes(1024)
    # Encrypt the plaintext, decrypt is
    ciphertext = stream_cipher_instance.encrypt(plaintext)
    obtained_plaintext = stream_cipher_instance.decrypt(ciphertext)
    # Verify the decrypted ciphertext equals our original plaintext
    assert_true(obtained_plaintext == plaintext)
Example #10
0
def challenge_54():
    # Set parameters, define hash function
    k = 4  # 2^4 = 16
    md_hash = lambda x, y: MerkleDamgard(x, 2, y)
    START_IV = b'\x00' * 16

    # Generate `2^k` random messages with their hashes, then add them as leaves to the hash/message tree
    hash_tree, msg_tree = [[]], [[]]
    for _ in range(2**k):
        msg = set2.random_bytes(16)
        msg_tree[0].append(msg)
        hash_tree[0].append(md_hash(msg, START_IV))

    # Generate full hash/message tree
    print('k = {}'.format(k))
    with tqdm(desc="Generating hash tree", total=(2**k) - 1) as progress:
        hash_tree, msg_tree = generate_hash_tree(hash_tree, msg_tree, md_hash,
                                                 progress)
    # Make claim based on the root of the hash tree
    hash_value = hash_tree[-1][0]
    print('> I hereby claim the hash will be equal to {}!'.format(hash_value))

    # Now, _after_ we have made our claim, craft our message with the match results
    msg = b'Ajax-PSV=3-0; Feyenoord-RKC=0-15'
    # Generate hash of our message
    msg_hash = md_hash(msg, START_IV)
    # Our crafted message starts with the original message
    crafted_msg = msg
    # Find a collision between our message's hash and one of the leaves in the hash tree
    glue, glue_hash = None, None
    while glue_hash not in hash_tree[0]:
        glue = set2.random_bytes(16)
        glue_hash = md_hash(glue, msg_hash)
    # Append the glue to our crafted message
    crafted_msg += glue
    # Find the index of the leaf we found a collision against
    index = hash_tree[0].index(glue_hash)
    # Now find the remaining `k` blocks that will eventually hash to `hash_value`
    for level in range(1, k + 1):
        index = index // 2
        crafted_msg += msg_tree[level][index]
    # Verify our crafted message hashes to the hash value we claimed it would have
    assert_true(md_hash(crafted_msg, START_IV) == hash_value)
Example #11
0
def challenge_51():
    # Prepare a Stream Cipher compression oracle and an AES-CBC compression oracle
    stream_oracle = lambda msg: compression_oracle(
        msg, lambda x: set3.encrypt_aes_ctr(
            x, key=set2.random_bytes(16), nonce=random.randint(2**8, 2**16)))
    cbc_oracle = lambda msg: compression_oracle(
        msg, lambda x: set2.encrypt_aes_cbc(
            x, key=set2.random_bytes(16), iv=set2.random_bytes(16)))

    # Define the base text
    base = 'POST / HTTP/1.1\r\nHost: hapless.com\r\nCookie: sessionid='

    # Find the tokens using the Stream Cipher
    print('Stream Cipher: ', end='')
    assert_true(find_token(base, oracle=stream_oracle)[len(base):] == token)

    # AES-CBC is more challenging, as it works with blocks.
    # To detect a successful guess, we'll have to add padding which will make 'wrong' guesses one block longer than the correct one
    print('AES-CBC:       ', end='')
    assert_true(find_token(base, oracle=cbc_oracle)[len(base):] == token)
Example #12
0
def challenge_31():
    # Generate random key
    key = set2.random_bytes(16)
    filename = b'filename'
    # Set up web server
    _thread.start_new_thread(webserver, (key, ))
    # Generate the hmac we're looking for
    correct_hmac = hmac_sha1(key, filename).hexdigest()
    # Try to guess the hmac using our timing attack
    guessed_hmac = retrieve_valid_hmac(filename)
    # Verify the guessed hmac is equal to the correct hmac
    assert_true(correct_hmac == guessed_hmac)
Example #13
0
def challenge_32():
    # Generate random key
    key = set2.random_bytes(16)
    filename = b'filename'
    # Set up web server, but now with a very small 'insecure_comparison' sleep time
    _thread.start_new_thread(webserver, (key, 0.01))
    # Generate the hmac we're looking for
    correct_hmac = hmac_sha1(key, filename).hexdigest()
    # Try to guess the hmac using our timing attack, but now with 10 rounds for each guess
    guessed_hmac = retrieve_valid_hmac(filename, 10)
    # Verify the guessed hmac is equal to the correct hmac
    assert_true(correct_hmac == guessed_hmac)
Example #14
0
def FindCollisions(n):
    # Define our hash functions `f` and `g` (cheap and expensive, respectively)
    cheap_hash = lambda x, y: MerkleDamgard(x, 2, y)
    expensive_hash = lambda x: MerkleDamgard(x, 3, b'\x00' * 3)

    while True:
        hash_map = {}
        collisions = []
        # Start Phase 1: generate 2^n hash collisions using `f`
        with tqdm(desc="Phase 1", total=int(2**n)) as pbar:
            H = b'\x00' * 2
            while len(collisions) < 2**n:
                # Generate random message
                message = set2.random_bytes(16)
                # Get hash value
                hash_value = cheap_hash(message, H)
                # If a new collision was found, update the progress bar
                if hash_value in hash_map and len(
                        hash_map[hash_value]
                ) == 1 and hash_map[hash_value][0] != message:
                    H = hash_value
                    # If it is the first collision, simply add it to the list
                    if len(collisions) < 1:
                        pbar.update(1)
                        collisions.append(hash_map[hash_value] + [message])
                    else:
                        # If it isn't, 'double' the existing collisions by simply appending our new message
                        for c in list(collisions):
                            #assert cheap_hash(c[0] + message, b'\x00'*2) == cheap_hash(c[1] + message, b'\x00'*2)
                            pbar.update(1)
                            collisions.append([c[0] + message, c[1] + message])
                    hash_map = {}
                else:
                    # Add found hash and message to `hash_map`, which keeps track of all hashes and corresponding messages
                    hash_map[hash_value] = hash_map.get(hash_value,
                                                        []) + [message]

        # Start Phase 2: for the found 2^n collisions, try all pairs and see if they also collide under `g`
        with tqdm(desc="Phase 2", total=len(collisions)) as pbar:
            for collision in collisions:
                pbar.update(1)
                # If they collide, we're done
                if expensive_hash(collision[0]) == expensive_hash(
                        collision[1]):
                    pbar.close()
                    print("Collision found!")
                    return
            else:
                # If no collisions under `g` were found, we have to start over
                pbar.set_description("Phase 2: No collision found (restart)")
                n += 2
Example #15
0
def challenge_26():
    # Intialise the encryption parameters
    nonce = random.randrange(1, 10**10)
    key = set2.random_bytes(16)

    # Generate ciphertext for our string
    ciphertext = bytearray(challenge_26_encryption('-admin-true', key, nonce))

    # Replace the 32th character with the XOR of itself with '-' and ';'
    # Note that AES CTR will XOR this again with the key stream,
    # hence resulting in ';'. Same strategy for the 38th character.
    ciphertext[32] = ciphertext[32] ^ ord('-') ^ ord(';')
    ciphertext[38] = ciphertext[38] ^ ord('-') ^ ord('=')

    assert_true(challenge_26_admin_check(bytes(ciphertext), key, nonce))
Example #16
0
def challenge_17():
    # Initialise key
    key = set2.random_bytes(16)
    # Call function_1 to get random ciphertext with IV used
    ciphertext, iv = function_1(key)
    # For verification purposes, decrypt given ciphertext with key
    expected = set2.decrypt_aes_cbc(ciphertext, key, iv)
    print('Expected: {}'.format(expected))
    # Initialise oracle function
    oracle = lambda x: function_2(x, key, iv)
    # Run oracle attack
    result = aes_oracle_attack(oracle, ciphertext, iv)
    print('Found:    {}'.format(bytes(result)))
    # Verify found answer equals what we're expecting
    assert_true(result == expected)
    print("")
Example #17
0
def function_1(key):
    string_set = [
        "MDAwMDAwTm93IHRoYXQgdGhlIHBhcnR5IGlzIGp1bXBpbmc=",
        "MDAwMDAxV2l0aCB0aGUgYmFzcyBraWNrZWQgaW4gYW5kIHRoZSBWZWdhJ3MgYXJlIHB1bXBpbic=",
        "MDAwMDAyUXVpY2sgdG8gdGhlIHBvaW50LCB0byB0aGUgcG9pbnQsIG5vIGZha2luZw==",
        "MDAwMDAzQ29va2luZyBNQydzIGxpa2UgYSBwb3VuZCBvZiBiYWNvbg==",
        "MDAwMDA0QnVybmluZyAnZW0sIGlmIHlvdSBhaW4ndCBxdWljayBhbmQgbmltYmxl",
        "MDAwMDA1SSBnbyBjcmF6eSB3aGVuIEkgaGVhciBhIGN5bWJhbA==",
        "MDAwMDA2QW5kIGEgaGlnaCBoYXQgd2l0aCBhIHNvdXBlZCB1cCB0ZW1wbw==",
        "MDAwMDA3SSdtIG9uIGEgcm9sbCwgaXQncyB0aW1lIHRvIGdvIHNvbG8=",
        "MDAwMDA4b2xsaW4nIGluIG15IGZpdmUgcG9pbnQgb2g=",
        "MDAwMDA5aXRoIG15IHJhZy10b3AgZG93biBzbyBteSBoYWlyIGNhbiBibG93"
    ]
    selected_string = base64.b64decode(random.choice(string_set))
    iv = set2.random_bytes(16)
    ciphertext = set2.encrypt_aes_cbc(selected_string, key, iv)
    return ciphertext, iv
Example #18
0
def MitM_alter_group(g):
    alice = EchoAlice2()
    bob = EchoBob2()
    eve = EchoEve2(g)

    secret_msg = set2.random_bytes(50)
    msg1 = alice.initiate(nist_p, nist_g)
    msg1p = eve.intercept_alice_initiate(msg1)
    msg2 = bob.receive(msg1p)
    msg2p = eve.intercept_bob_receive(msg2)
    msg3 = alice.send_A(msg2p)
    msg4 = bob.send_B(msg3)
    msg5 = alice.send_msg(msg4, secret_msg)
    msg5p = eve.intercept_alice_send_msg(msg5)
    #msg6 = bob.receive_msg(msg5p)
    exchange_2_cracked = secret_msg == eve.decrypt_message() or MitM_alter_group(g)
    return exchange_2_cracked
Example #19
0
def challenge_27():
    # Intialise the encryption parameters
    key = set2.random_bytes(16)
    # Generate ciphertext for our string
    ciphertext = challenge_27_encryption('whatevah', key)
    # Modify the ciphertext such that it is equal to c_1 + 0 + c_2
    ciphertext = ciphertext[0:16] + b'\x00' * 16 + ciphertext[0:16]
    try:
        # Verify this modified ciphertext will now parse 'admin':'true'
        assert_true(not challenge_27_check_decrypt(ciphertext, key))
    except ValueError as error:
        # Extract the deciphered text from the exception
        plaintext = error.args[1]
        # XOR p_1 with p_3
        derived_key = bytes(
            [x ^ y for x, y in zip(plaintext[0:16], plaintext[32:48])])
        # Verify the derived key equals the original key
        assert_true(derived_key == key)
Example #20
0
def challenge_25():
    key = set2.random_bytes(16)
    nonce = random.randrange(1, 10**10)
    # Get plaintext
    with open('inputs/25.txt') as file:
        contents = base64.b64decode(file.read())
    plaintext = set2.pkcs7_remove_padding(
        set1.decrypt_aes_ecb(contents, 'YELLOW SUBMARINE'))
    # Generate ciphertext
    ciphertext = set3.encrypt_aes_ctr(plaintext, key, nonce)

    # Create vulnerable function
    def vulnerable_ctr_stream_edit_function(ciphertext, offset, newtext):
        return edit_ctr_stream(ciphertext, key, nonce, offset, newtext)

    # Find the plaintext using the above function
    obtained_plaintext = find_ctr_plaintext(
        ciphertext, vulnerable_ctr_stream_edit_function)
    # Verify found plaintext matches with the original plaintext
    assert_true(obtained_plaintext == plaintext)
Example #21
0
def challenge_29():
    # Generate random key of random number of bytes
    key = set2.random_bytes(random.randrange(4, 40))

    # Prepare validation function
    def validate_mac(message, digest):
        return simple_mac(key, message,
                          False).hexdigest() == digest.hexdigest()

    # Prepare message to sign
    msg = b"comment1=cooking%20MCs;userdata=foo;comment2=%20like%20a%20pound%20of%20bacon"
    # Generate genuine mac of the message
    original_mac = simple_mac(key, msg, False)
    # Obtain a, b, c, d and e
    abcde = original_mac._digest()
    # Prepare string to inject
    string_to_inject = b";admin=true"
    # Create a forged mac:
    # - The forged length is {msg} and it's padding (hence a multiple of 64), plus the length of our injected string
    # - We feed the SHA-1 instance the [a-e] parameters obtained from the original mac
    forged_mac = external.slowsha.SHA1(
        string_to_inject,
        math.ceil(len(msg) / 64) * 64 + len(string_to_inject), abcde)
    # To figure out what message we actually signed, we need to figure out what the padding was of the original mac
    # Because we don't know the key, we have to guess the key length
    for key_length_guess in range(4, 41):
        # Compose the candidate message based on the key length guess
        signed_msg_guess = msg + get_sha1_padding(b'\x00' * key_length_guess +
                                                  msg) + string_to_inject
        # Verify the validity of the guessed message with the forged mac
        if validate_mac(signed_msg_guess, forged_mac):
            print(
                "Successfully forged message - key used had length {}".format(
                    key_length_guess))
            assert_true(True)
            return
    raise Exception("Could not forge MAC")
Example #22
0
def challenge_19():
    # Set fixed once, random key, and initialise plaintexts and ciphertexts
    nonce = 0
    key = set2.random_bytes(16)
    plaintexts = [
        'SSBoYXZlIG1ldCB0aGVtIGF0IGNsb3NlIG9mIGRheQ==',
        'Q29taW5nIHdpdGggdml2aWQgZmFjZXM=',
        'RnJvbSBjb3VudGVyIG9yIGRlc2sgYW1vbmcgZ3JleQ==',
        'RWlnaHRlZW50aC1jZW50dXJ5IGhvdXNlcy4=',
        'SSBoYXZlIHBhc3NlZCB3aXRoIGEgbm9kIG9mIHRoZSBoZWFk',
        'T3IgcG9saXRlIG1lYW5pbmdsZXNzIHdvcmRzLA==',
        'T3IgaGF2ZSBsaW5nZXJlZCBhd2hpbGUgYW5kIHNhaWQ=',
        'UG9saXRlIG1lYW5pbmdsZXNzIHdvcmRzLA==',
        'QW5kIHRob3VnaHQgYmVmb3JlIEkgaGFkIGRvbmU=',
        'T2YgYSBtb2NraW5nIHRhbGUgb3IgYSBnaWJl', 'VG8gcGxlYXNlIGEgY29tcGFuaW9u',
        'QXJvdW5kIHRoZSBmaXJlIGF0IHRoZSBjbHViLA==',
        'QmVpbmcgY2VydGFpbiB0aGF0IHRoZXkgYW5kIEk=',
        'QnV0IGxpdmVkIHdoZXJlIG1vdGxleSBpcyB3b3JuOg==',
        'QWxsIGNoYW5nZWQsIGNoYW5nZWQgdXR0ZXJseTo=',
        'QSB0ZXJyaWJsZSBiZWF1dHkgaXMgYm9ybi4=',
        'VGhhdCB3b21hbidzIGRheXMgd2VyZSBzcGVudA==',
        'SW4gaWdub3JhbnQgZ29vZCB3aWxsLA==', 'SGVyIG5pZ2h0cyBpbiBhcmd1bWVudA==',
        'VW50aWwgaGVyIHZvaWNlIGdyZXcgc2hyaWxsLg==',
        'V2hhdCB2b2ljZSBtb3JlIHN3ZWV0IHRoYW4gaGVycw==',
        'V2hlbiB5b3VuZyBhbmQgYmVhdXRpZnVsLA==', 'U2hlIHJvZGUgdG8gaGFycmllcnM/',
        'VGhpcyBtYW4gaGFkIGtlcHQgYSBzY2hvb2w=',
        'QW5kIHJvZGUgb3VyIHdpbmdlZCBob3JzZS4=',
        'VGhpcyBvdGhlciBoaXMgaGVscGVyIGFuZCBmcmllbmQ=',
        'V2FzIGNvbWluZyBpbnRvIGhpcyBmb3JjZTs=',
        'SGUgbWlnaHQgaGF2ZSB3b24gZmFtZSBpbiB0aGUgZW5kLA==',
        'U28gc2Vuc2l0aXZlIGhpcyBuYXR1cmUgc2VlbWVkLA==',
        'U28gZGFyaW5nIGFuZCBzd2VldCBoaXMgdGhvdWdodC4=',
        'VGhpcyBvdGhlciBtYW4gSSBoYWQgZHJlYW1lZA==',
        'QSBkcnVua2VuLCB2YWluLWdsb3Jpb3VzIGxvdXQu',
        'SGUgaGFkIGRvbmUgbW9zdCBiaXR0ZXIgd3Jvbmc=',
        'VG8gc29tZSB3aG8gYXJlIG5lYXIgbXkgaGVhcnQs',
        'WWV0IEkgbnVtYmVyIGhpbSBpbiB0aGUgc29uZzs=',
        'SGUsIHRvbywgaGFzIHJlc2lnbmVkIGhpcyBwYXJ0',
        'SW4gdGhlIGNhc3VhbCBjb21lZHk7',
        'SGUsIHRvbywgaGFzIGJlZW4gY2hhbmdlZCBpbiBoaXMgdHVybiw=',
        'VHJhbnNmb3JtZWQgdXR0ZXJseTo=', 'QSB0ZXJyaWJsZSBiZWF1dHkgaXMgYm9ybi4='
    ]
    ciphertexts = []

    # Create ciphertexts using set key and nonce
    for plaintext in plaintexts:
        ciphertexts.append(
            encrypt_aes_ctr(base64.b64decode(plaintext), key, nonce))

    # Obtain guessed plaintexts
    guessed_plaintexts = crack_aes_ctr(ciphertexts)

    distance = lambda x, y: sum([1 if x != y else 0 for x, y in zip(x, y)])
    incorrect_byte_count = sum([
        distance(base64.b64decode(x), y)
        for x, y in zip(plaintexts, guessed_plaintexts)
    ])
    total_byte_count = sum([len(x) for x in plaintexts])

    # Determine the accuracy of this approach by dividing the number of incorrect guessed plaintext bytes by the total number of bytes
    accuracy = 1 - (incorrect_byte_count / total_byte_count)
    print('Accuracy: {:.2%}'.format(accuracy))
    # Not perfect, but enough to manually debug and find the full original text

    assert_true(accuracy > 0.95)