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)
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)
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
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))
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))
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))
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
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
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)
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)
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)
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)
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)
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
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))
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("")
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
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
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)
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)
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")
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)