Esempio n. 1
0
def test_cbc_bitflip_attack():
    # Set 2, challenge 16 CBC Bitflipping
    # Note: I did this assuming the prepended text was known by the attacker.

    block_size = BLOCK_SIZE
    key = gen_random_block()
    iv = gen_random_block()

    prepend = "comment1=cooking%20MCs;userdata="
    append = ";comment2=%20like%20a%20pound%20of%20bacon"

    # Make sure ; and = are quoted
    # Selecting user controlled value exactly twice the target text
    plaintext = "123456789012123456789012".replace(";", "").replace("=", "")
    full_plaintext = prepend + plaintext + append
    ciphertext = cbc_encrypt_prepend_and_append(
        key,
        iv,
        plaintext.encode("utf-8"),
        append.encode("utf-8"),
        prepend.encode("utf-8"),
    )

    modified_ciphertext = b""
    target_text = ";admin=true;"
    edit_start_position = 0
    edit_stop_position = len(target_text)

    # Now tweak the bytes in the first ciphertext (comment field) such that the change
    # is introduced in the second plaintext.
    for ind, by in enumerate(ciphertext):
        if ind in range(edit_start_position, edit_stop_position):
            new_value = bytes([
                int.from_bytes(
                    target_text[edit_start_position + ind].encode("utf-8"),
                    "big") ^ ciphertext[ind]
                ^ int.from_bytes(
                    full_plaintext[ind + block_size].encode("utf-8"), "big")
            ])

            modified_ciphertext = modified_ciphertext + new_value
        else:
            modified_ciphertext = modified_ciphertext + bytes([by])

    decrypted_plaintext = aes_cbc_decrypt(key, modified_ciphertext, iv)

    assert target_text.encode("utf-8") in decrypted_plaintext
Esempio n. 2
0
def test_cbc_recover_key_with_iv_eq_key():
    # Set 4, challenge 27: Recover the key from CBC with IV=Key
    key = gen_random_block()
    plaintext = "comment1=cooking%20MCs;userdata=1234567890123456"

    # Verify each byte of the plaintext for ASCII compliance
    # (ie, look for high-ASCII values). Noncompliant messages
    # should raise an exception or return an error that includes
    # the decrypted plaintext.
    def verify_plaintext(plaintext: bytes):
        # Check for high-ASCII (bytes 127 or higher)
        for b in plaintext:
            assert b in range(32, 126)

    verify_plaintext(plaintext.encode("utf8"))
    ciphertext = aes_cbc_encrypt(key, plaintext.encode("utf8"), key)
    block_1 = ciphertext[:BLOCK_SIZE]
    block_4 = ciphertext[BLOCK_SIZE * 3:]
    all_zero = b"\x00" * BLOCK_SIZE

    # Modify the message (you are now the attacker):
    # C_1, C_2, C_3 -> C_1, 0, C_1
    new_ciphertext = block_1 + all_zero + block_1 + block_4

    # This will raise BadPaddingValidation on block_4
    decrypted_plaintext = aes_cbc_decrypt(key,
                                          new_ciphertext,
                                          key,
                                          remove_padding=False)
    try:
        verify_plaintext(decrypted_plaintext)
    except AssertionError:  # And this will happen as the attacker tampered with the ciphertext!
        pass

    # As the attacker, recovering the plaintext from the error, extract the key
    # P'_1 XOR P'_3

    p_1_prime = decrypted_plaintext[:BLOCK_SIZE]
    p_3_prime = decrypted_plaintext[BLOCK_SIZE * 2:BLOCK_SIZE * 3]
    reconstructed_key = b""
    for x, y in zip(p_1_prime, p_3_prime):
        byte = bytes([x ^ y])
        reconstructed_key += byte

    # Check we reconstructed the key successfully
    for x, y in zip(key, reconstructed_key):
        assert x == y
Esempio n. 3
0
def test_ctr_bitflip_attack():
    # Set 4, challenge 26 CTR Bitflipping

    key = gen_random_block()
    nonce = 0

    prepend = "comment1=cooking%20MCs;userdata="
    append = ";comment2=%20like%20a%20pound%20of%20bacon"

    # Make sure ; and = are quoted
    # Selecting user controlled value exactly the target text
    plaintext = "123456789012".replace(";", "").replace("=", "")
    full_plaintext = prepend + plaintext + append
    ciphertext = ctr_encrypt_prepend_and_append(
        key,
        nonce,
        plaintext.encode("utf-8"),
        append.encode("utf-8"),
        prepend.encode("utf-8"),
    )

    modified_ciphertext = b""
    target_text = ";admin=true;"

    # We start editing at the beginning of user controlled plaintext
    edit_start_position = len(prepend) + 1
    edit_stop_position = edit_start_position + len(target_text)

    # Now tweak the bytes in the ciphertext (comment field). Easier than CBC mode
    # since we target the location exactly instead of the prior block.
    for ind, by in enumerate(ciphertext):
        if ind in range(edit_start_position, edit_stop_position):
            new_value = bytes([
                int.from_bytes(target_text[0].encode("utf-8"), "big")
                ^ ciphertext[ind]
                ^ int.from_bytes(full_plaintext[ind].encode("utf-8"), "big")
            ])
            target_text = target_text[1:]

            modified_ciphertext = modified_ciphertext + new_value
        else:
            modified_ciphertext = modified_ciphertext + bytes([by])

    decrypted_plaintext = aes_ctr_decrypt(key, modified_ciphertext, nonce)

    assert target_text.encode("utf-8") in decrypted_plaintext
Esempio n. 4
0
def test_ecb_cut_and_paste_cookie():
    # Set 2, challenge 13

    block_size = 16
    key = gen_random_block(block_size)

    original_cookie = generate_profile_for("*****@*****.**")
    aes_ecb_encrypt(key, original_cookie.encode("utf-8"))

    # Get a block containing (we'll take block two from the ciphertext):
    # email=blahhhhhhh | admin            | &uid...

    oracle_cookie = generate_profile_for("blahhhhhhhadmin           ")
    encrypted_oracle_cookie = aes_ecb_encrypt(key,
                                              oracle_cookie.encode("utf-8"))

    # Get a ciphertext with role at the block boundary
    # so we can strip off the existing role:
    #                 |                |
    # email=blaaaaaaaaaaah&uid=10&role=

    cut_and_pasted_cookie = generate_profile_for("blaaaaaaaaaah")
    encrypted_cut_and_pasted_cookie = aes_ecb_encrypt(
        key, cut_and_pasted_cookie.encode("utf-8"))

    # Since the ECB plaintexts are padded, we need to also add a block
    # of padding on the end such that the padding will validate.
    #                 |                |
    # email=blaaaaaah&uid=10&role=user

    padding_cookie = generate_profile_for("blaaaaaah")
    encrypted_padding_cookie = aes_ecb_encrypt(key,
                                               padding_cookie.encode("utf-8"))

    block_to_paste = encrypted_oracle_cookie[block_size:2 * block_size]
    block_to_cut = encrypted_cut_and_pasted_cookie[0:-1 * block_size]
    block_to_pad = encrypted_padding_cookie[-1 * block_size:]
    encrypted_admin_profile = block_to_cut + block_to_paste + block_to_pad

    decrypted_admin_profile = aes_ecb_decrypt(key, encrypted_admin_profile)
    admin_profile = parse_structured_cookie(
        decrypted_admin_profile.decode("utf-8"))

    assert admin_profile["role"] == "admin"
Esempio n. 5
0
def test_ecb_cbc_detection_oracle():
    # Set 2, challenge 11: Detect ECB or CBC

    num_ecbs = 0
    num_cbcs = 0
    num_total_iterations = 100

    plaintext = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".encode("utf-8")

    for _ in range(num_total_iterations):
        key = gen_random_block()
        ciphertext = encryption_ecb_cbc_detection_oracle(key, plaintext)

        if detect_ecb_use(ciphertext):
            num_ecbs += 1
        else:
            num_cbcs += 1

    cbc_rate = num_cbcs / num_total_iterations
    ecb_rate = num_ecbs / num_total_iterations

    # Expect 50 ECBs, 50 CBCs
    assert cbc_rate > 0.40 and cbc_rate < 0.60
    assert ecb_rate > 0.40 and ecb_rate < 0.60
Esempio n. 6
0
def test_random_prefix_byte_at_a_time_ecb_decryption():
    # Set 2, challenge 14: Byte-at-a-time ECB decryption (harder)

    path_to_test_data = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), "data/12.txt")

    with open(path_to_test_data, "r") as f:
        append_text_str = f.read()

    append_bytes = base64_to_bytes(append_text_str)

    # We won't actually use the key anywhere other than to pass it to the
    # oracle function.
    key = gen_random_block()

    # Get random number of bytes. This we will prepend to the plaintext.
    random_number_of_bytes = random.randint(1, 15)
    prepend_bytes = os.urandom(random_number_of_bytes)

    blocksize = 16

    ciphertext = ecb_encrypt_prepend_and_append(key, b"", append_bytes,
                                                prepend_bytes)

    test_str = "A" * blocksize
    ciphertext_of_all_As = aes_ecb_encrypt(
        key, test_str.encode("utf-8"))[0:blocksize]

    # We don't really know where to start in the ciphertext. Previously we started
    # decrypting with the first ciphertext block, when we manipulated it to
    # contain one target character.
    # We need to first determine how much space the prefix bytes are taking up.
    block_to_begin_at = None
    number_of_characters_in_test_string = None
    for num_of_test_characters in range(blocksize * 2 - 1, 1, -1):
        test_str = "A" * num_of_test_characters

        ciphertext = ecb_encrypt_prepend_and_append(key,
                                                    test_str.encode("utf-8"),
                                                    append_bytes,
                                                    prepend_bytes)

        for block_num in range(1, len(ciphertext) // blocksize):

            if (ciphertext_of_all_As == ciphertext[(block_num - 1) *
                                                   blocksize:block_num *
                                                   blocksize]):
                block_to_begin_at = block_num
                number_of_characters_in_test_string = num_of_test_characters

    # We add sufficient characters in the target block so that the unknown prefix
    # bytes fill a block boundary. Then we start at that block boundary and decrypt
    # as before.
    reconstructed_str = ""

    for index_of_target_block in range(block_to_begin_at,
                                       len(ciphertext) // blocksize):

        bytes_so_far_this_block = b""
        for test_byte in range(blocksize):

            if index_of_target_block == block_to_begin_at:
                # If it's the first block, we control the prefix bytes.
                attacker_controlled_bytes = "A" * (blocksize - test_byte - 1)

                # Prefix is used for the dict calculation
                prefix = (attacker_controlled_bytes.encode("utf-8") +
                          bytes_so_far_this_block)

                # Add our prefix to pad the unknown bytes to the block boundary.
                attacker_controlled_bytes = (
                    "A" * number_of_characters_in_test_string +
                    attacker_controlled_bytes)
            else:
                # But if it's the second block or later, we need to use the
                # bytes we reconstructed from the previous block as the prefix.
                previous_block_bytes = reconstructed_str[-1 * (blocksize - 1):]

                # The prefix is used for the dict calculation
                prefix = previous_block_bytes.encode("utf-8")

                # Add our prefix to pad the unknown bytes to the block boundary.
                attacker_controlled_bytes = (
                    "A" * number_of_characters_in_test_string + "A" *
                    (blocksize - test_byte - 1))

            ciphertext = ecb_encrypt_prepend_and_append(
                key,
                attacker_controlled_bytes.encode("utf-8"),
                append_bytes,
                prepend_bytes,
            )

            cipher_dict = construct_ecb_attack_dict(key, prefix)

            target_block_ciphertext = ciphertext[
                (index_of_target_block - 1) * blocksize:index_of_target_block *
                blocksize]

            last_char = cipher_dict[target_block_ciphertext]

            reconstructed_str = reconstructed_str + last_char
            bytes_so_far_this_block = bytes_so_far_this_block + last_char.encode(
                "utf-8")

    assert "With my rag-top down so my hair can blow" in reconstructed_str
Esempio n. 7
0
def test_byte_at_a_time_ecb_decryption():
    # Set 2, challenge 12: Byte-at-a-time ECB decryption (Simple)

    path_to_test_data = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), "data/12.txt")

    with open(path_to_test_data, "r") as f:
        append_text_str = f.read()

    append_bytes = base64_to_bytes(append_text_str)

    # We won't actually use the key anywhere other than to pass it to the
    # oracle function ecb_encrypt_append.
    key = gen_random_block()

    # Determine blocksize
    previous_ciphertext_len = None
    test_input = "A"
    for blocksize in range(100):
        test_input = "A" + test_input
        test_ciphertext = ecb_encrypt_append(key, test_input.encode("utf-8"),
                                             append_bytes)

        if (previous_ciphertext_len
                and len(test_ciphertext) - previous_ciphertext_len != 0):
            blocksize = len(test_ciphertext) - previous_ciphertext_len
            break

        previous_ciphertext_len = len(test_ciphertext)

    assert blocksize == BLOCK_SIZE  # Check we inferred blocksize correctly.

    ciphertext = ecb_encrypt_append(key, b"", append_bytes)

    # Now we make repeated calls (as described in problem statement) to
    # pass inputs that are (blocksize - 1) in length.
    reconstructed_str = ""

    for index_of_target_block in range(1, len(ciphertext) // blocksize):

        bytes_so_far_this_block = b""
        for test_byte in range(blocksize):

            if index_of_target_block == 1:
                # If it's the first block, we control the prefix bytes.
                attacker_controlled_bytes = "A" * (blocksize - test_byte - 1)

                # Prefix is used for the dict calculation
                prefix = (attacker_controlled_bytes.encode("utf-8") +
                          bytes_so_far_this_block)
            else:
                # But if it's the second block or later, we need to use the
                # bytes we reconstructed from the previous block as the prefix.
                previous_block_bytes = reconstructed_str[-1 * (blocksize - 1):]

                # The prefix is used for the dict calculation
                prefix = previous_block_bytes.encode("utf-8")

                # Attacker-controlled bytes here are just to make sure there is only
                # a single unknown character in the target block.
                attacker_controlled_bytes = "A" * (blocksize - test_byte - 1)

            ciphertext = ecb_encrypt_append(
                key, attacker_controlled_bytes.encode("utf-8"), append_bytes)

            cipher_dict = construct_ecb_attack_dict(key, prefix)

            target_block_ciphertext = ciphertext[
                (index_of_target_block - 1) * blocksize:index_of_target_block *
                blocksize]

            last_char = cipher_dict[target_block_ciphertext]

            reconstructed_str = reconstructed_str + last_char
            bytes_so_far_this_block = bytes_so_far_this_block + last_char.encode(
                "utf-8")

    assert "With my rag-top down so my hair can blow" in reconstructed_str