def detect_mode(ciphertext): blocks = challenge_07.as_blocks(ciphertext, 16) if challenge_08.detect_aes_ecb(blocks)[1] > 1: return "ecb" else: return "cbc"
def forge_padding_block(oracle): """ Given an oracle, forges a block with all PKCS#7 padding (which occurs when the length of a plaintext is an integer multiple of the block size) """ b_size, pt_size, padding = challenge_12.determine_block_stats(oracle) new_padding = b"A" * padding return challenge_07.as_blocks(oracle(new_padding), b_size)[-1]
def recover_key(ciphertext, oracle): """ Uses a decryption oracle to recover the IV, which in this case happens to also be the key. Let: C'₁ = C₁ C'₂ = 0x00 C'₃ = C₁ And consider that: P'₃ = D(C'₃) ⊕ C'₂ P'₃ = D(C₁) ⊕ C'₂ P'₃ = D(C₁) ⊕ 0x00 P'₃ = D(C₁) P'₁ = D(C₁) ⊕ IV Which implies that: P'₁ = P'₃ ⊕ IV IV = P'₁ ⊕ P'₃ """ blocks = challenge_07.as_blocks(ciphertext, 16) key = b"" error_msg = b"" # Leak the exception containing the plaintext to the attacker. try: oracle(blocks[0] + (b"\x00" * 16) + blocks[0] + b"".join(blocks[1:5])) except Exception as e: error_msg = str(e) # Offset for actual error string is 17, which is all we really care about error_msg = error_msg[17:].encode('iso-8859-1') plaintext = challenge_07.as_blocks(error_msg, 16) # Recover the IV (the key) for i, c in enumerate(plaintext[0]): key += bytes([c ^ plaintext[2][i]]) return key
def forge_block(offset, plaintext, oracle): """ Given an offset, plaintext, and oracle, forges a block with the proper padding. """ b_size, _, _ = challenge_12.determine_block_stats(oracle) new_padding = b"A" * (b_size - offset) payload = new_padding + challenge_09.pkcs7(plaintext, b_size) ciphertext = oracle(payload) return challenge_07.as_blocks(ciphertext, b_size)[1]
def decrypt(self, ciphertext): if len(ciphertext) % 16 != 0: raise ValueError("Invalid length of ciphertext") # Break the ciphertext up into blocks blocks = challenge_07.as_blocks(ciphertext, 16) # All blocks are decrypted individually for i in range(len(blocks)): blocks[i] = self.cipher.decrypt(blocks[i]) return challenge_09.remove_pkcs7(b''.join(blocks), 16)
def encrypt(self, plaintext): # Apply padding so all blocks end up as 16 bytes plaintext = challenge_09.pkcs7(plaintext, 16) # Break the plaintext up into blocks blocks = challenge_07.as_blocks(plaintext, 16) # All blocks are encrypted individually for i in range(len(blocks)): blocks[i] = self.cipher.encrypt(blocks[i]) return b''.join(blocks)
def determine_offset(oracle, block_size): """ Determines the offset in which user input starts within the ciphertext by analyzing what injected padding causes a repeated block. """ for i in range(1, 128): ciphertext = oracle(b"A" * i) blocks = challenge_07.as_blocks(ciphertext, block_size) last = None for idx, block in enumerate(blocks): if block == last: return idx * block_size - (i - block_size) last = block
def encrypt(self, plaintext, nonce): blocks = challenge_07.as_blocks(plaintext, 16) keystream = b"" ciphertext = b"" # Produce the blocks we need for our keystream for i in range(len(blocks)): nonce_padded = nonce + b"\x00" * (8 - len(nonce)) counter = bytes([i]) counter_padded = counter + b"\x00" * (8 - len(counter)) keystream += self.cipher.encrypt(nonce_padded + counter_padded) for i, c in enumerate(plaintext): ciphertext += bytes([c ^ keystream[i]]) return ciphertext
def encrypt(self, plaintext): # Apply padding so all blocks end up as 16 bytes plaintext = challenge_09.pkcs7(plaintext, 16) # Break the plaintext up into blocks blocks = challenge_07.as_blocks(plaintext, 16) # First block is the result of encrypt(blocks[0] ^ IV) blocks[0] = self.cipher.encrypt( challenge_02.fixed_xor(blocks[0], self.iv)) # All other blocks are the result of encrypt(blocks[i] ^ blocks[i-1]) for i in range(1, len(blocks)): blocks[i] = self.cipher.encrypt( challenge_02.fixed_xor(blocks[i], blocks[i - 1])) return b''.join(blocks)
def decrypt(self, ciphertext): if len(ciphertext) % 16 != 0: raise ValueError("Invalid length of ciphertext") # Break the ciphertext up into blocks blocks = challenge_07.as_blocks(ciphertext, 16) decrypted = blocks # All but the first decrypted block are the result of # decrypt(blocks[i]) ^ blocks[i-1] for i in range(len(decrypted) - 1, 0, -1): decrypted[i] = challenge_02.fixed_xor( self.cipher.decrypt(blocks[i]), blocks[i - 1]) # First decrypted block is the result of decrypt(blocks[0]) ^ IV decrypted[0] = challenge_02.fixed_xor(self.cipher.decrypt(blocks[0]), self.iv) return challenge_09.remove_pkcs7(b''.join(decrypted), 16)
def decrypt_with_padding_oracle(oracle): ciphertext, iv = get_ciphertext_and_iv() blocks = [bytes(iv)] + challenge_07.as_blocks(ciphertext, 16) plaintext = b"" for i in range(len(blocks) - 1, 0, -1): block_p = b"" # Recovered plaintext of current block for j in range(15, -1, -1): cprime_k = b"" # Last k bytes of C′, the block we control pkcs = 16 - j # PKCS#7 padding char (our P′ value) for k in range(15 - j): # P′ᵢ[k] = Pᵢ[k] ⊕ Cᵢ₋₁[k] ⊕ C′[k] # => C′[k] = P′ᵢ[k] ⊕ Pᵢ[k] ⊕ Cᵢ₋₁[k] cprime_k = bytes([pkcs ^ block_p[k] ^ blocks[i-1][15-k]]) + \ cprime_k for k in range(256): padding = b"0" * j guess = bytes([k]) c_prime = padding + guess + cprime_k # If C′||Cᵢ has valid padding, we know C′[j] if oracle(c_prime + blocks[i]): # We now have enough info to solve for Pᵢ[j] in # # P′ᵢ[j] = Pᵢ[j] ⊕ Cᵢ₋₁[j] ⊕ C′[j] # => Pᵢ[j] = P′ᵢ[j] ⊕ Cᵢ₋₁[j] ⊕ C′[j] # p_char = bytes([pkcs ^ blocks[i - 1][j] ^ ord(guess)]) block_p += p_char plaintext = p_char + plaintext break return challenge_09.remove_pkcs7(plaintext, 16)
def sanitize_email(email): return email.translate({ord(c): None for c in '&='}) def parse_key_value(str1): return dict(item.split("=") for item in str1.split("&")) if __name__ == '__main__': # Forge a block with an offset of 6 to compensate for "email=" forgery = forge_block(6, b"admin", new_profile) # When email length is 13, the second block ends in "role=" ciphertext = new_profile(b"*****@*****.**") b_size, _, _ = challenge_12.determine_block_stats(new_profile) blocks = challenge_07.as_blocks(ciphertext, b_size) # "Cut and paste" blocks to provide the desired ciphertext profile1 = get_profile(blocks[0] + blocks[1] + forgery) print(profile1) # Try another way of doing this. This version relies on generating a block # full of PKCS#7 padding and doing more block "cut and pasting" # When input length is 13, the second block ends in "role=". The last 3 # chars will become part of the email ciphertext = new_profile(b"AAAAAAAAAAcom") role_block = challenge_07.as_blocks(ciphertext, b_size)[1] # When input length is 15, the second block starts with last 5 chars
import base64 import challenge_07 def detect_aes_ecb(blocks): histogram = {} for block in blocks: histogram[block] = histogram.get(block, 0) + 1 block = max(histogram, key=histogram.get) return block, histogram[block] if __name__ == '__main__': max_score = 0 block = b"" for line in open("08.txt", "r"): ciphertext = base64.b64decode(line) blocks = challenge_07.as_blocks(ciphertext, 16) guessed_block, score = detect_aes_ecb(blocks) if score > max_score: max_score = score block = guessed_block print("Likely AES ECB Block: {}".format(block)) print("Highest Score : {}".format(max_score))