def challenge_14() -> object: """ Take your oracle function from #12. Now generate a random count of random bytes and prepend this string to every plaintext. You are now doing: AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key) :return: """ base64_encoded = ( "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGll" "cyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK" ) base64_decoded = binascii.a2b_base64(base64_encoded) random_aes_key = cryptopals.generate_random_bytes(16) random_prepend = cryptopals.generate_random_bytes(random.randint(1, 15)) encrypted_blocks = cryptopals.encrypt_ecb_oracle( b"", base64_decoded, random_aes_key, prepend=random_prepend ) # print("Padding: {}".format(obtain_ecb_pkcs7_count(base64_decoded, random_aes_key, prepend=random_prepend))) cryptopals.obtain_ecb_prepend_padding_count( base64_decoded, random_aes_key, prepend=random_prepend ) # print(decrypt_aes(b''.join(encrypted_blocks), random_aes_key)) # print("Original Encrypted Blocks: {}".format(len(encrypted_blocks))) result = cryptopals.decrypt_ecb_message_without_key( encrypted_blocks, base64_decoded, random_aes_key, prepend=random_prepend ) assert base64_decoded == result.strip( b"\x01" ), "Decryption failed! {} != {}".format(base64_decoded, result)
def challenge_14() -> object: """ Take your oracle function from #12. Now generate a random count of random bytes and prepend this string to every plaintext. You are now doing: AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key) :return: """ base64_encoded = ( "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGll" "cyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK" ) base64_decoded = binascii.a2b_base64(base64_encoded) random_aes_key = cryptopals.generate_random_bytes(16) random_prepend = cryptopals.generate_random_bytes(random.randint(1, 15)) encrypted_blocks = cryptopals.encrypt_ecb_oracle(b"", base64_decoded, random_aes_key, prepend=random_prepend) # print("Padding: {}".format(obtain_ecb_pkcs7_count(base64_decoded, random_aes_key, prepend=random_prepend))) cryptopals.obtain_ecb_prepend_padding_count(base64_decoded, random_aes_key, prepend=random_prepend) # print(decrypt_aes(b''.join(encrypted_blocks), random_aes_key)) # print("Original Encrypted Blocks: {}".format(len(encrypted_blocks))) result = cryptopals.decrypt_ecb_message_without_key(encrypted_blocks, base64_decoded, random_aes_key, prepend=random_prepend) assert base64_decoded == result.strip( b"\x01"), "Decryption failed! {} != {}".format(base64_decoded, result)
def encrypt_oracle(text: bytes): random_aes_key = cryptopals.generate_random_bytes(16) prefix = cryptopals.generate_random_bytes(random.randint(5, 10)) postfix = cryptopals.generate_random_bytes(random.randint(5, 10)) message = b"".join([prefix, text, postfix]) if random.randint(1, 2) == 2: encoding_type = "ECB" # encrypt ECB keysize = len(random_aes_key) text = cryptopals.pkcs_7_padding(text, keysize) blocks = [text[n : n + keysize] for n in range(0, len(text), keysize)] encrypted = [] for block in blocks: text = cryptopals.encrypt_aes(block, random_aes_key) # print(binascii.hexlify(text)) encrypted.append(text) else: # encrypt_CBC encoding_type = "CBC" random_iv = cryptopals.generate_random_bytes(16) encrypted = cryptopals.encrypt_aes_with_custom_cbc( message, random_aes_key, random_iv ) return encrypted, encoding_type
def encrypt_oracle(text: bytes): random_aes_key = cryptopals.generate_random_bytes(16) prefix = cryptopals.generate_random_bytes(random.randint(5, 10)) postfix = cryptopals.generate_random_bytes(random.randint(5, 10)) message = b"".join([prefix, text, postfix]) if random.randint(1, 2) == 2: encoding_type = "ECB" # encrypt ECB keysize = len(random_aes_key) text = cryptopals.pkcs_7_padding(text, keysize) blocks = [ text[n:n + keysize] for n in range(0, len(text), keysize) ] encrypted = [] for block in blocks: text = cryptopals.encrypt_aes(block, random_aes_key) # print(binascii.hexlify(text)) encrypted.append(text) else: # encrypt_CBC encoding_type = "CBC" random_iv = cryptopals.generate_random_bytes(16) encrypted = cryptopals.encrypt_aes_with_custom_cbc( message, random_aes_key, random_iv) return encrypted, encoding_type
class Server: iv = cryptopals.generate_random_bytes(16) key = cryptopals.generate_random_bytes(16) test_data = [ b"MDAwMDAwTm93IHRoYXQgdGhlIHBhcnR5IGlzIGp1bXBpbmc =", b"MDAwMDAxV2l0aCB0aGUgYmFzcyBraWNrZWQgaW4gYW5kIHRoZSBWZWdhJ3MgYXJlIHB1bXBpbic =", b"MDAwMDAyUXVpY2sgdG8gdGhlIHBvaW50LCB0byB0aGUgcG9pbnQsIG5vIGZha2luZw ==", b"MDAwMDAzQ29va2luZyBNQydzIGxpa2UgYSBwb3VuZCBvZiBiYWNvbg ==", b"MDAwMDA0QnVybmluZyAnZW0sIGlmIHlvdSBhaW4ndCBxdWljayBhbmQgbmltYmxl", b"MDAwMDA1SSBnbyBjcmF6eSB3aGVuIEkgaGVhciBhIGN5bWJhbA ==", b"MDAwMDA2QW5kIGEgaGlnaCBoYXQgd2l0aCBhIHNvdXBlZCB1cCB0ZW1wbw ==", b"MDAwMDA3SSdtIG9uIGEgcm9sbCwgaXQncyB0aW1lIHRvIGdvIHNvbG8 =", b"MDAwMDA4b2xsaW4nIGluIG15IGZpdmUgcG9pbnQgb2g =", b"MDAwMDA5aXRoIG15IHJhZy10b3AgZG93biBzbyBteSBoYWlyIGNhbiBibG93", ] def get_encrypted_blocks(self): # The first function should # - select at random one of ten strings # - generate a random AES key (which it should save for all future encryptions), # - pad the string out to the 16-byte AES block size and # - CBC-encrypt it under that key, # - providing the caller the ciphertext and IV. # grab a random string random_string = self.test_data[random.randrange( 0, len(self.test_data))] print("-" * 128) print("PRIOR TO ENCRYPTION: {}".format(random_string)) print("PRIOR TO ENCRBASE64: {}".format( binascii.a2b_base64(random_string))) # encrypt using CBC encrypted_blocks = cryptopals.encrypt_aes_with_custom_cbc( random_string, self.key, self.iv) return encrypted_blocks def decrypt_cookie(self, ciphertext): # Consume the ciphertext # decrypt it, # check its padding, and # return true or false depending on whether the padding is valid. blocks = cryptopals.decrypt_aes_with_custom_cbc( ciphertext, self.key, self.iv) message = b"".join(blocks) # print("{} (Decrypted)".format(message)) try: cryptopals.pkcs_7_padding_verification(message) except ValueError: return False return True
def challenge_20() -> None: """ Using 20.txt, find a similar set of Base64'd plaintext. Do with them exactly what you did with the first, but solve the problem differently. Instead of making spot guesses at to known plaintext, treat the collection of ciphertexts the same way you would repeating-key XOR. Obviously, CTR encryption appears different from repeated-ke XOR, but with a fixed nonce they are effectively the same thing. To exploit this: 1. take your collection of ciphertexts and 2. truncate them to a common length (the length of the smallest ciphertext will work). Solve the resulting concatenation of ciphertexts as if for repeating- key XOR, with a key size of the length of the ciphertext you XOR'd. """ key = cryptopals.generate_random_bytes(16) nonce = 0 with open("data/20.txt", "r") as handle: lines = [ binascii.a2b_base64(line.strip()) for line in handle.readlines() ] crypts = [ cryptopals.aes_with_custom_ctr(line, key, nonce) for line in lines ] # min_length = len(max(crypts, key=len)) blocks = [crypt for crypt in crypts] transposed = cryptopals.transpose(blocks) # check each index for possible hits # if more than one hit -- check for score. keys = {} for index, block in enumerate(transposed): keys[index] = [] for guess in range(255): items = [(item ^ guess) for item in block] count = [ item for item in items if chr(item) in (string.ascii_letters + " ,.'?:-;") ] # if index == 10: # print(len(items), len(count), [chr(item) for item in items]) keys[index].append([len(count), bytes([guess])]) key = [] for index in keys: key.append(max(keys[index], key=lambda x: x[0])[1]) result = b"".join(key) print(result) for block in blocks: print(cryptopals.decrypt_xor(block, result))
def challenge_12() -> None: """ Byte - at - a - time ECB decryption(Simple) Copy your oracle function to a new function that encrypts buffers under ECB mode using a consistent but unknown key (for instance, assign a single random key, once, to a global variable). Now take that same function and have it append to the plaintext, BEFORE ENCRYPTING, the following string: Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK Spoiler alert. Do not decode this string now. Don't do it. Base64 decode the string before appending it. Do not base64 decode the string by hand; make your code do it.The point is that you don't know its contents. What you have now is a function that produces: AES - 128 - ECB(your - string | | unknown - string, random - key) It turns out: you can decrypt "unknown-string" with repeated calls to the oracle function! Here's roughly how: 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. 1. Discover the block size of the cipher. You know it, but do this step anyway 2. Detect that the function is using ECB. You already know, but do this step anyways. 3. Knowing the block size, craft an input block that is exactly 1 byte short (for instance, if the block size is 8 bytes, make "AAAAAAA"). Think about what the oracle function is going to put in that last byte position. 4. Make a dictionary of every possible last byte by feeding different strings to the oracle; for instance, "AAAAAAAA", "AAAAAAAB", "AAAAAAAC", remembering the first block of each invocation. 5. Match the output of the one-byte-short input to one of the entries in your dictionary. You've now discovered the first byte of unknown-string. 6. Repeat for the next byte. """ base64_encoded = ( "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGll" "cyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK" ) base64_decoded = binascii.a2b_base64( base64_encoded) # base64.b64decode(base64_encoded) random_aes_key = cryptopals.generate_random_bytes(16) encrypted_blocks = cryptopals.encrypt_ecb_oracle(b"", base64_decoded, random_aes_key) result = cryptopals.decrypt_ecb_message_without_key( encrypted_blocks, base64_decoded, random_aes_key) assert ( result == b"Rollin' in my 5.0\nWith my rag-top down so my hair can blow\nThe girlies on standby" b" waving just to say hi\nDid you stop? No, I just drove by\n\x01" ), "Decryption Failed!"
def challenge_20() -> None: """ Using 20.txt, find a similar set of Base64'd plaintext. Do with them exactly what you did with the first, but solve the problem differently. Instead of making spot guesses at to known plaintext, treat the collection of ciphertexts the same way you would repeating-key XOR. Obviously, CTR encryption appears different from repeated-ke XOR, but with a fixed nonce they are effectively the same thing. To exploit this: 1. take your collection of ciphertexts and 2. truncate them to a common length (the length of the smallest ciphertext will work). Solve the resulting concatenation of ciphertexts as if for repeating- key XOR, with a key size of the length of the ciphertext you XOR'd. """ key = cryptopals.generate_random_bytes(16) nonce = 0 with open("data/20.txt", "r") as handle: lines = [binascii.a2b_base64(line.strip()) for line in handle.readlines()] crypts = [cryptopals.aes_with_custom_ctr(line, key, nonce) for line in lines] # min_length = len(max(crypts, key=len)) blocks = [crypt for crypt in crypts] transposed = cryptopals.transpose(blocks) # check each index for possible hits # if more than one hit -- check for score. keys = {} for index, block in enumerate(transposed): keys[index] = [] for guess in range(255): items = [(item ^ guess) for item in block] count = [ item for item in items if chr(item) in (string.ascii_letters + " ,.'?:-;") ] # if index == 10: # print(len(items), len(count), [chr(item) for item in items]) keys[index].append([len(count), bytes([guess])]) key = [] for index in keys: key.append(max(keys[index], key=lambda x: x[0])[1]) result = b"".join(key) print(result) for block in blocks: print(cryptopals.decrypt_xor(block, result))
def challenge_19() -> None: """ Attack this cryptosystem piecemeal: guess letters, use expected English language frequence to validate guesses, catch common English trigrams, and so on. :return: """ lines = [ "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=", ] # Generate a random key and encrypt the above base64 text random_key = cryptopals.generate_random_bytes(16) encrypted_messages = [] for item in lines: message = binascii.a2b_base64(item) encrypted = cryptopals.aes_with_custom_ctr(message, random_key, nonce=0) encrypted_messages.append(encrypted) """ foo_BYTE XOR bar_BYTE = KEYSTREAM-BYTE And since the keystream is the same for every ciphertext: foo-BYTE XOR KEYSTREAM-BYTE = bar-BYTE """ key = [] max_line_length = len(max(encrypted_messages, key=len)) for index in range(0, max_line_length): scores = {} for guess in range(256): items = [ chr(message[index] ^ guess) for message in encrypted_messages if len(message) > index ] score = cryptopals.english.score_text("".join(items).encode()) scores[guess] = score high_score = max(scores, key=lambda x: scores[x]) if high_score > 0: key.append(bytes([high_score])) test_total_decrypt = [] final_key = b"".join(key) keysize = len(final_key) for line, message in zip(lines, encrypted_messages): blocks = [message[n : n + keysize] for n in range(0, len(message), keysize)] decrypt = [cryptopals.decrypt_xor(block, final_key) for block in blocks] test_total_decrypt.append( cryptopals.compute_hamming_distance( b"".join(decrypt), binascii.a2b_base64(line) ) ) normalized_distance = sum(test_total_decrypt) / len(test_total_decrypt) assert ( normalized_distance < 5 ), "Hamming Distance of {} suggests decryption failed.".format(normalized_distance)
def challenge_16() -> None: """ CBC bitflipping attacks Generate a random AES key. Combine your padding code and CBC code to write two functions. The first function should take an arbitrary input string, prepend the string: "comment1=cooking%20MCs;userdata=" .. and append the string: ";comment2=%20like%20a%20pound%20of%20bacon" The function should quote out the ";" and "=" characters. The function should then pad out the input to the 16-byte AES block length and encrypt it under the random AES key. The second function should decrypt the string and look for the characters ";admin=true;" (or, equivalently, decrypt, split the string on ";", convert each resulting string into 2-tuples, and look for the "admin" tuple). Return true or false based on whether the string exists. If you've written the first function properly, it should not be possible to provide user input to it that will generate the string the second function is looking for. We'll have to break the crypto to do that. Instead, modify the ciphertext (without knowledge of the AES key) to accomplish this. You're relying on the fact that in CBC mode, a 1-bit error in a ciphertext block: Completely scrambles the block the error occurs in Produces the identical 1-bit error(/edit) in the next ciphertext block. Stop and think for a second. Before you implement this attack, answer this question: why does CBC mode have this property? :return: """ def encrypt_using_cbc(message, key): prepend = r"comment1=cooking%20MCs;userdata=" cleaned = message.replace(";", "").replace("=", "") append = r";comment2=%20like%20a%20pound%20of%20bacon" full_message = "{}{}{}".format(prepend, cleaned, append).encode() encrypted = cryptopals.encrypt_aes_with_custom_cbc( full_message, key, b"YELLOW SUBMARINE" ) return encrypted def is_admin(encrypted, random_aes_key): decrypted = cryptopals.decrypt_aes_with_custom_cbc( b"".join(encrypted), random_aes_key, b"YELLOW SUBMARINE" ) # print('FUN: {}'.format(decrypted[2])) return False if b"".join(decrypted).find(b";admin=true;") == -1 else True random_aes_key = cryptopals.generate_random_bytes(16) encrypted = encrypt_using_cbc("?admin?true", random_aes_key) # for debugging info # decrypted = decrypt_aes_with_custom_cbc(b''.join(encrypted), random_aes_key, b'YELLOW SUBMARINE') # do something here to make ;admin=true exist in encrypted. success = False for index in range(0, 255): first_array = bytearray(encrypted[1]) first_array[0] = index for index2 in range(0, 255): first_array[6] = index2 encrypted[1] = bytes(first_array) result = is_admin(encrypted, random_aes_key) if result: # print("Hacked Using the following byte Manipulations: {}, {}".format(index, index2)) success = True break if success: break assert success is True, "Unable to hack admin gate!"
def challenge_13() -> None: """ ECB cut-and-paste :return: """ def profile_for(user_input: str): """ Now write a function that encodes a user profile in that format, given an email address. You should have something like: profile_for("*****@*****.**") ... and it should produce: { email: '*****@*****.**', uid: 10, role: 'user' } ... encoded as: [email protected]&uid=10&role=user Your "profile_for" function should not allow encoding metacharacters (& and =). Eat them, quote them, whatever you want to do, but don't let people set their email address to "[email protected]&role=admin". :return: """ # Eat illegals illegals = "&=" for illegal in illegals: user_input.replace(illegal, "") user_profile = collections.OrderedDict() user_profile["email"] = user_input user_profile["uid"] = 10 user_profile["role"] = "user" items = ["{0}={1}".format(k, v) for k, v in user_profile.items()] user_text = "&".join(items) return user_text email = ( "theadminisfake.test@gmail." + "admin{}".format("\x11" * 11) + "com" ) # necessary to push 'user' to its own line profile = profile_for(email) cookie = cryptopals.create_structured_cookie(profile) # print(cookie) """ Now, two more easy functions. Generate a random AES key, then: A. Encrypt the encoded user profile under the key; "provide" that to the "attacker". B. Decrypt the encoded user profile and parse it. Using only the user input to profile_for() (as an oracle to generate "valid" ciphertexts) and the ciphertexts themselves, make a role=admin profile. """ random_aes_key = cryptopals.generate_random_bytes(16) keysize = len(random_aes_key) message = cryptopals.pkcs_7_padding(profile.encode(), keysize) for_attacker = cryptopals.encrypt_aes(message, random_aes_key) # print("For Attacker: {}".format(for_attacker)) # to_be_swizzled = pkcs_7_padding(for_attacker, len(random_aes_key)) to_be_swizzled = [ for_attacker[n : n + keysize] for n in range(0, len(for_attacker), keysize) ] # Reorder the ECB Blocks and throw away the regular user account :) final = list() final.append(to_be_swizzled[0]) final.append(to_be_swizzled[1]) final.append(to_be_swizzled[3]) final.append(to_be_swizzled[2]) for_me = cryptopals.decrypt_aes(b"".join(final), random_aes_key) assert ( for_me == b"[email protected]&uid=10&" b"role=admin\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11" ), "Admin account could not be hacked!"
def challenge_12() -> None: """ Byte - at - a - time ECB decryption(Simple) Copy your oracle function to a new function that encrypts buffers under ECB mode using a consistent but unknown key (for instance, assign a single random key, once, to a global variable). Now take that same function and have it append to the plaintext, BEFORE ENCRYPTING, the following string: Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK Spoiler alert. Do not decode this string now. Don't do it. Base64 decode the string before appending it. Do not base64 decode the string by hand; make your code do it.The point is that you don't know its contents. What you have now is a function that produces: AES - 128 - ECB(your - string | | unknown - string, random - key) It turns out: you can decrypt "unknown-string" with repeated calls to the oracle function! Here's roughly how: 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. 1. Discover the block size of the cipher. You know it, but do this step anyway 2. Detect that the function is using ECB. You already know, but do this step anyways. 3. Knowing the block size, craft an input block that is exactly 1 byte short (for instance, if the block size is 8 bytes, make "AAAAAAA"). Think about what the oracle function is going to put in that last byte position. 4. Make a dictionary of every possible last byte by feeding different strings to the oracle; for instance, "AAAAAAAA", "AAAAAAAB", "AAAAAAAC", remembering the first block of each invocation. 5. Match the output of the one-byte-short input to one of the entries in your dictionary. You've now discovered the first byte of unknown-string. 6. Repeat for the next byte. """ base64_encoded = ( "Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkgaGFpciBjYW4gYmxvdwpUaGUgZ2lybGll" "cyBvbiBzdGFuZGJ5IHdhdmluZyBqdXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUgYnkK" ) base64_decoded = binascii.a2b_base64( base64_encoded ) # base64.b64decode(base64_encoded) random_aes_key = cryptopals.generate_random_bytes(16) encrypted_blocks = cryptopals.encrypt_ecb_oracle( b"", base64_decoded, random_aes_key ) result = cryptopals.decrypt_ecb_message_without_key( encrypted_blocks, base64_decoded, random_aes_key ) assert ( result == b"Rollin' in my 5.0\nWith my rag-top down so my hair can blow\nThe girlies on standby" b" waving just to say hi\nDid you stop? No, I just drove by\n\x01" ), "Decryption Failed!"
def challenge_19() -> None: """ Attack this cryptosystem piecemeal: guess letters, use expected English language frequence to validate guesses, catch common English trigrams, and so on. :return: """ lines = [ "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=", ] # Generate a random key and encrypt the above base64 text random_key = cryptopals.generate_random_bytes(16) encrypted_messages = [] for item in lines: message = binascii.a2b_base64(item) encrypted = cryptopals.aes_with_custom_ctr(message, random_key, nonce=0) encrypted_messages.append(encrypted) """ foo_BYTE XOR bar_BYTE = KEYSTREAM-BYTE And since the keystream is the same for every ciphertext: foo-BYTE XOR KEYSTREAM-BYTE = bar-BYTE """ key = [] max_line_length = len(max(encrypted_messages, key=len)) for index in range(0, max_line_length): scores = {} for guess in range(256): items = [ chr(message[index] ^ guess) for message in encrypted_messages if len(message) > index ] score = cryptopals.english.score_text("".join(items).encode()) scores[guess] = score high_score = max(scores, key=lambda x: scores[x]) if high_score > 0: key.append(bytes([high_score])) test_total_decrypt = [] final_key = b"".join(key) keysize = len(final_key) for line, message in zip(lines, encrypted_messages): blocks = [ message[n:n + keysize] for n in range(0, len(message), keysize) ] decrypt = [ cryptopals.decrypt_xor(block, final_key) for block in blocks ] test_total_decrypt.append( cryptopals.compute_hamming_distance(b"".join(decrypt), binascii.a2b_base64(line))) normalized_distance = sum(test_total_decrypt) / len(test_total_decrypt) assert (normalized_distance < 5), "Hamming Distance of {} suggests decryption failed.".format( normalized_distance)
def challenge_16() -> None: """ CBC bitflipping attacks Generate a random AES key. Combine your padding code and CBC code to write two functions. The first function should take an arbitrary input string, prepend the string: "comment1=cooking%20MCs;userdata=" .. and append the string: ";comment2=%20like%20a%20pound%20of%20bacon" The function should quote out the ";" and "=" characters. The function should then pad out the input to the 16-byte AES block length and encrypt it under the random AES key. The second function should decrypt the string and look for the characters ";admin=true;" (or, equivalently, decrypt, split the string on ";", convert each resulting string into 2-tuples, and look for the "admin" tuple). Return true or false based on whether the string exists. If you've written the first function properly, it should not be possible to provide user input to it that will generate the string the second function is looking for. We'll have to break the crypto to do that. Instead, modify the ciphertext (without knowledge of the AES key) to accomplish this. You're relying on the fact that in CBC mode, a 1-bit error in a ciphertext block: Completely scrambles the block the error occurs in Produces the identical 1-bit error(/edit) in the next ciphertext block. Stop and think for a second. Before you implement this attack, answer this question: why does CBC mode have this property? :return: """ def encrypt_using_cbc(message, key): prepend = r"comment1=cooking%20MCs;userdata=" cleaned = message.replace(";", "").replace("=", "") append = r";comment2=%20like%20a%20pound%20of%20bacon" full_message = "{}{}{}".format(prepend, cleaned, append).encode() encrypted = cryptopals.encrypt_aes_with_custom_cbc( full_message, key, b"YELLOW SUBMARINE") return encrypted def is_admin(encrypted, random_aes_key): decrypted = cryptopals.decrypt_aes_with_custom_cbc( b"".join(encrypted), random_aes_key, b"YELLOW SUBMARINE") # print('FUN: {}'.format(decrypted[2])) return False if b"".join(decrypted).find( b";admin=true;") == -1 else True random_aes_key = cryptopals.generate_random_bytes(16) encrypted = encrypt_using_cbc("?admin?true", random_aes_key) # for debugging info # decrypted = decrypt_aes_with_custom_cbc(b''.join(encrypted), random_aes_key, b'YELLOW SUBMARINE') # do something here to make ;admin=true exist in encrypted. success = False for index in range(0, 255): first_array = bytearray(encrypted[1]) first_array[0] = index for index2 in range(0, 255): first_array[6] = index2 encrypted[1] = bytes(first_array) result = is_admin(encrypted, random_aes_key) if result: # print("Hacked Using the following byte Manipulations: {}, {}".format(index, index2)) success = True break if success: break assert success is True, "Unable to hack admin gate!"
def challenge_13() -> None: """ ECB cut-and-paste :return: """ def profile_for(user_input: str): """ Now write a function that encodes a user profile in that format, given an email address. You should have something like: profile_for("*****@*****.**") ... and it should produce: { email: '*****@*****.**', uid: 10, role: 'user' } ... encoded as: [email protected]&uid=10&role=user Your "profile_for" function should not allow encoding metacharacters (& and =). Eat them, quote them, whatever you want to do, but don't let people set their email address to "[email protected]&role=admin". :return: """ # Eat illegals illegals = "&=" for illegal in illegals: user_input.replace(illegal, "") user_profile = collections.OrderedDict() user_profile["email"] = user_input user_profile["uid"] = 10 user_profile["role"] = "user" items = ["{0}={1}".format(k, v) for k, v in user_profile.items()] user_text = "&".join(items) return user_text email = ("theadminisfake.test@gmail." + "admin{}".format("\x11" * 11) + "com") # necessary to push 'user' to its own line profile = profile_for(email) cookie = cryptopals.create_structured_cookie(profile) # print(cookie) """ Now, two more easy functions. Generate a random AES key, then: A. Encrypt the encoded user profile under the key; "provide" that to the "attacker". B. Decrypt the encoded user profile and parse it. Using only the user input to profile_for() (as an oracle to generate "valid" ciphertexts) and the ciphertexts themselves, make a role=admin profile. """ random_aes_key = cryptopals.generate_random_bytes(16) keysize = len(random_aes_key) message = cryptopals.pkcs_7_padding(profile.encode(), keysize) for_attacker = cryptopals.encrypt_aes(message, random_aes_key) # print("For Attacker: {}".format(for_attacker)) # to_be_swizzled = pkcs_7_padding(for_attacker, len(random_aes_key)) to_be_swizzled = [ for_attacker[n:n + keysize] for n in range(0, len(for_attacker), keysize) ] # Reorder the ECB Blocks and throw away the regular user account :) final = list() final.append(to_be_swizzled[0]) final.append(to_be_swizzled[1]) final.append(to_be_swizzled[3]) final.append(to_be_swizzled[2]) for_me = cryptopals.decrypt_aes(b"".join(final), random_aes_key) assert (for_me == b"[email protected]&uid=10&" b"role=admin\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11" ), "Admin account could not be hacked!"