def known_plaintexts(pairs, ciphertext, block_size=16): """Given enough pairs plaintext-ciphertext, we can assign ciphertexts blocks to plaintexts blocks, then we can possibly decrypt ciphertext Args: pairs(list): list of dict, [{'cipher': 'aaa', 'plain': 'bbb'}, {'cipher': 'xxx', 'plain': 'pwa'}] plaintexts have to be correctly padded (len(cipher) == len(plain)) ciphertext(string): ciphertext to decrypt block_size(int) Returns tuple: ([decrypted_ciphertext_blocks], {'ciphertext_block': 'plaintext_block', ...}) decrypted_ciphertext_blocks may contain not-decrypted blocks from ciphertext """ result_mapping = {} for pair in pairs: ciphertext_blocks = chunks(pair['cipher'], block_size) plaintext_blocks = chunks(pair['plain'], block_size) if len(ciphertext_blocks) != len(plaintext_blocks): print(pair) print(ciphertext_blocks, plaintext_blocks) print(len(ciphertext_blocks), len(plaintext_blocks)) assert 0 for cipher_block_no in range(len(ciphertext_blocks)): result_mapping[ciphertext_blocks[cipher_block_no]] = plaintext_blocks[cipher_block_no] target_ciphertext_blocks = chunks(ciphertext, block_size) for cipher_block_no in range(len(target_ciphertext_blocks)): if target_ciphertext_blocks[cipher_block_no] in list(result_mapping.keys()): target_ciphertext_blocks[cipher_block_no] = result_mapping[target_ciphertext_blocks[cipher_block_no]] return target_ciphertext_blocks, result_mapping
def fake_ciphertext(new_plaintext, padding_oracle=None, decryption_oracle=None, block_size=16): """Make ciphertext that will decrypt to given plaintext Give padding_oracle or decryption_oracle (or both) Args: new_plaintext(string): with padding padding_oracle(function/None) decryption_oracle(function/None): maximum one block to decrypt block_size(int) Returns: fake_ciphertext(string): fake ciphertext that will decrypt to new_plaintext """ _check_oracles(padding_oracle=padding_oracle, decryption_oracle=decryption_oracle, block_size=block_size) if block_size % 8 != 0: log.critical_error("Incorrect block size: {}".format(block_size)) log.info("Start fake ciphertext") ciphertext = bytes(b'A' * (len(new_plaintext) + block_size)) # prepare blocks blocks = chunks(ciphertext, block_size) new_pl_blocks = chunks(new_plaintext, block_size) if len(new_pl_blocks) != len(blocks) - 1: log.critical_error( "Wrong new plaintext length({}), should be {}".format( len(new_plaintext), block_size * (len(blocks) - 1))) new_ct_blocks = list(blocks) # add known plaintext for count_block in range(len(blocks) - 1, 0, -1): """ Every block, modify block[count_block-1] to set block[count_block] """ log.info("Block no. {}".format(count_block)) ciphertext_to_decrypt = bytes(b''.join(new_ct_blocks[:count_block + 1])) original_plaintext = decrypt(ciphertext_to_decrypt, padding_oracle=padding_oracle, decryption_oracle=decryption_oracle, block_size=block_size, amount=1, is_correct=False) log.info("Set block no. {}".format(count_block)) new_ct_blocks[count_block - 1] = xor(blocks[count_block - 1], original_plaintext, new_pl_blocks[count_block - 1]) fake_ciphertext_res = bytes(b''.join(new_ct_blocks)) log.success("Fake ciphertext(hex): {}".format(b2h(fake_ciphertext_res))) return fake_ciphertext_res
def find_prefix_suffix_size(encryption_oracle, block_size=16): """Determine prefix and suffix sizes if ecb mode, sizes must be constant Rarely may fail (if random data that are send unhappily matches prefix/suffix) Args: encryption_oracle(callable) block_size(int) Returns: tuple(int,int): prefix_size, suffix_size """ blocks_to_send = 5 payload = random_bytes(1) * (blocks_to_send * block_size) enc_chunks = chunks(encryption_oracle(payload), block_size) log.debug("Encryption of length {}".format(blocks_to_send * block_size)) log.debug(print_chunks(enc_chunks)) for position_start in range(len(enc_chunks) - 1): if enc_chunks[position_start] == enc_chunks[position_start + 1]: for y in range(2, blocks_to_send - 1): if enc_chunks[position_start] != enc_chunks[position_start + y]: break else: log.success("Controlled payload start at chunk {}".format(position_start)) break else: log.critical_error("Position of controlled chunks not found") log.info('Finding prefix') changed_char = bytes([(payload[0] - 1)%256]) for aligned_bytes in range(block_size): payload_new = payload[:aligned_bytes] + changed_char + payload[aligned_bytes+1:] enc_chunks_new = chunks(encryption_oracle(payload_new), block_size) log.debug(print_chunks(chunks(payload_new, block_size))) log.debug(print_chunks(enc_chunks_new)) if enc_chunks_new[position_start] != enc_chunks[position_start]: prefix_size = position_start*block_size - aligned_bytes log.success("Prefix size: {}".format(prefix_size)) break else: log.critical_error("Size of prefix not found") log.info('Finding suffix') payload = random_bytes(1) * (block_size - (prefix_size % block_size)) # align to block_size encrypted = encryption_oracle(payload) suffix_size = len(encrypted) - len(payload) - prefix_size while True: payload += random_bytes(1) suffix_size -= 1 if len(encryption_oracle(payload)) > len(encrypted): log.success("Suffix size: {}".format(suffix_size)) break else: log.critical_error("Size of suffix not found") return prefix_size, suffix_size
def length_extension(old_hash, size, new_message, type='sha1'): """Length extension attack: given hash(secret) and len(secret), compute new_hash and old_padding so that hash(secret+old_padding+new_message) == new_hash Args: old_hash(string): hash of secret value size(int): length of secret (in bytes) new_message(string) type(string): sha1 or md4 Returns: tuple: (new_hash, old_padding+new_message) """ implemented_functions = ['sha1', 'md4'] if type == 'sha1': endian = 'big' hash_function = sha1 elif type == 'md4': endian = 'little' hash_function = md4 else: log.critical_error("Not implemented, type must be one of {}".format( implemented_functions)) return None old_padding = add_md_padding(bytes(b'a' * size), endian=endian)[size:] new_data_size = len(new_message) + len(old_padding) + size new_padding = add_md_padding(bytes(b'a' * new_data_size), endian=endian)[new_data_size:] h = [b2i(x, endian=endian) for x in chunks(old_hash, 4)] return hash_function(new_message, h, new_padding), old_padding + new_message
def break_reuse_key(ciphertexts, lang='English', no_of_comparisons=5, alphabet=None, key_space=None, reliability=100.0): """Sentences xored with the same key Args: ciphertexts(list): texts xored with the same key lang(string): key in frequencies dict no_of_comparisons(int): used during comparing by frequencies alphabet(string/None): plaintext space key_space(string/None): key space reliability(float): between 0 and 100, used during comparing by frequencies Returns: list: sorted (by frequencies) list of tuples (key, list(plaintexts)) """ if len(ciphertexts) < 2: log.critical_error("Too less ciphertexts") min_size = min(list(map(len, ciphertexts))) ciphertexts = [one[:min_size] for one in ciphertexts] log.info("Ciphertexts shrinked to {} bytes".format(min_size)) pairs = break_repeated_key(''.join(ciphertexts), lang=lang, no_of_comparisons=no_of_comparisons, key_size=min_size, alphabet=alphabet, key_space=key_space, reliability=reliability) res = [(pair[0], chunks(pair[1], min_size)) for pair in pairs] return res
def iv_as_key(ciphertext, plaintext, padding_oracle=None, decryption_oracle=None, block_size=16): """If iv is used as key, we can recover it using decryption oracle, padding oracle or known plaintext (if first ciphertext block is repeated) Args: ciphertext(string): first block must be AES.encrypt(iv xor plaintext[0]) plaintext(string): with padding padding_oracle(function/None) decryption_oracle(function/None) block_size(int) Returns: string: key (== iv) """ key = None ciphertext = chunks(ciphertext, block_size) plaintext = chunks(plaintext, block_size) try: position_second = ciphertext.index(ciphertext[0], 1) log.debug("Position of the same block as the first is {}".format( position_second)) key = xor(plaintext[0], ciphertext[position_second - 1], plaintext[position_second]) except ValueError: log.debug( "first ciphertext block is not repeated, will use decryption/padding oracle" ) if key is None: iv = bytes(b'A' * block_size) iv_xor_plaintext0 = decrypt(ciphertext[0], padding_oracle=padding_oracle, decryption_oracle=decryption_oracle, iv=iv, block_size=block_size, is_correct=False, amount=1) iv_xor_plaintext0 = xor(iv_xor_plaintext0, iv) key = xor(iv_xor_plaintext0, plaintext[0]) log.success("Key(hex): {}".format(b2h(key))) return key
def is_ecb(cipher, block_size=16): """Check if there are repeated blocks in ciphertext Args: cipher(string) block_size(int) Returns: bool: True if there are repeated blocks (so it's probably ECB mode) """ cipher_blocks = chunks(cipher, block_size) unique_blocks = set(cipher_blocks) if len(unique_blocks) < len(cipher_blocks): return True return False
def compression_function_sha1(chunk, state): """Sha1 compression function Args: chunk(string): len(chunk) == 64 state(list of ints): len(state) == 5 Returns: list of ints: compressed state """ state = state[:] w = chunks(chunk, 4) w = list(map(b2i, w)) for i in range(16, 80): w.append( _left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1, 32)) a = state[0] b = state[1] c = state[2] d = state[3] e = state[4] for i in range(80): if i // 20 == 0: f = d ^ (b & (c ^ d)) k = 0x5A827999 elif i // 20 == 1: f = b ^ c ^ d k = 0x6ED9EBA1 elif i // 20 == 2: f = (b & c) | (b & d) | (c & d) k = 0x8F1BBCDC elif i // 20 == 3: f = b ^ c ^ d k = 0xCA62C1D6 a, b, c, d, e = (_left_rotate(a, 5, 32) + f + e + k + w[i]) & 0xffffffff, a, _left_rotate(b, 30, 32), c, d state[0] = (state[0] + a) & 0xffffffff state[1] = (state[1] + b) & 0xffffffff state[2] = (state[2] + c) & 0xffffffff state[3] = (state[3] + d) & 0xffffffff state[4] = (state[4] + e) & 0xffffffff return state
def merkle_damgard(data, initial_state, compression_function): """Merkle-Damgard construction Args: data(string) initial_state(list of ints) compression_function(function) Returns: final state(string) """ state = initial_state[:] data_chunks = chunks(data, 64) log.debug("Start merkle-damgard, chunks are: {}".format(data_chunks)) for chunk in data_chunks: log.debug("Process chunk: {} with state: {}".format(b2h(chunk), state)) state = compression_function(chunk, state) return state
def find_block_size(encryption_oracle, constant=True): """Determine block size if ecb mode Args: encryption_oracle(callable) constant(bool): True if prefix and suffix have constant length Returns: int """ if constant: log.debug("constant == True") payload = bytes(b'A') size = len(encryption_oracle(payload)) while True: payload += bytes(b'A') new_size = len(encryption_oracle(payload)) if new_size > size: log.info("block_size={}".format(new_size - size)) return new_size - size else: log.debug("constant == False") payload = bytes(b'A') max_size = len(encryption_oracle(payload)) possible_sizes = factors(max_size) possible_sizes.add(max_size) blocks_to_send = 5 for block_size in sorted(possible_sizes): """send payload of length x, so at least x-1 blocks should be identical""" payload = random_bytes(1) * (blocks_to_send*block_size) enc_chunks = chunks(encryption_oracle(payload), block_size) for x in range(len(enc_chunks)-1): if enc_chunks[x] == enc_chunks[x+1]: log.debug("Found two identical blocks at {}: {}".format(x, print_chunks(enc_chunks))) for y in range(2, blocks_to_send-1): if enc_chunks[x] != enc_chunks[x+y]: break else: log.info("block_size={}".format(block_size)) return block_size
def decrypt(encryption_oracle, constant=True, block_size=16, prefix_size=None, secret_size=None, alphabet=None): """Given encryption oracle which produce ecb(prefix || our_input || secret), find secret Args: encryption_oracle(callable) constant(bool): True if prefix have constant length (secret must have constant length) block_size(int/None) prefix_size(int/None) secret_size(int/None) alphabet(string): plaintext space Returns: secret(string) """ log.debug("Start decrypt function") if not alphabet: alphabet = bytes(string.printable.encode()) if not block_size: block_size = find_block_size(encryption_oracle, constant) if constant: log.debug("constant == True") if not prefix_size or not secret_size: prefix_size, secret_size = find_prefix_suffix_size(encryption_oracle, block_size) """Start decrypt""" secret = bytes(b'') aligned_bytes = random_bytes(1) * (block_size - (prefix_size % block_size)) if len(aligned_bytes) == block_size: aligned_bytes = bytes(b'') aligned_bytes_suffix = random_bytes(1) * (block_size - (secret_size % block_size)) if len(aligned_bytes_suffix) == block_size: aligned_bytes_suffix = bytes(b'') block_to_find_position = -1 controlled_block_position = (prefix_size+len(aligned_bytes)) // block_size while len(secret) < secret_size: if (len(secret)+1) % block_size == 0: block_to_find_position -= 1 payload = aligned_bytes + aligned_bytes_suffix + random_bytes(1) + secret enc_chunks = chunks(encryption_oracle(payload), block_size) block_to_find = enc_chunks[block_to_find_position] log.debug("To guess at position {}:".format(block_to_find_position)) log.debug("Plain: " + print_chunks(chunks(bytes(b'P'*prefix_size) + payload + bytes(b'S'*secret_size), block_size))) log.debug("Encry: " + print_chunks(enc_chunks)+"\n") for guessed_char in range(256): guessed_char = bytes([guessed_char]) payload = aligned_bytes + add_padding(guessed_char + secret, block_size) enc_chunks = chunks(encryption_oracle(payload), block_size) log.debug("Plain: " + print_chunks(chunks(bytes(b'P'*prefix_size) + payload + bytes(b'S'*secret_size), block_size))) log.debug("Encry: " + print_chunks(enc_chunks)+"\n") if block_to_find == enc_chunks[controlled_block_position]: secret = guessed_char + secret log.debug("Found char, secret={}".format(repr(secret))) break else: log.critical_error("Char not found, try change alphabet. Secret so far: {}".format(repr(secret))) log.success("Secret(hex): {}".format(b2h(secret))) return secret else: log.debug("constant == False")
def compression_function_md4(chunk, state): """MD4 compression function, taken from: https://gist.github.com/tristanwietsma/5937448 Args: chunk(string): len(chunk) == 64 state(list of ints): len(state) == 4 Returns: list of ints: compressed state """ def _f(x, y, z): return x & y | ~x & z def _g(x, y, z): return x & y | x & z | y & z def _h(x, y, z): return x ^ y ^ z def _f1(a, b, c, d, k, s, X): return _left_rotate(a + _f(b, c, d) + X[k], s) def _f2(a, b, c, d, k, s, X): return _left_rotate(a + _g(b, c, d) + X[k] + 0x5a827999, s) def _f3(a, b, c, d, k, s, X): return _left_rotate(a + _h(b, c, d) + X[k] + 0x6ed9eba1, s) state = state[:] x = chunks(chunk, 4) x = [b2i(one_x, endian='little') for one_x in x] a, b, c, d = state a = _f1(a, b, c, d, 0, 3, x) d = _f1(d, a, b, c, 1, 7, x) c = _f1(c, d, a, b, 2, 11, x) b = _f1(b, c, d, a, 3, 19, x) a = _f1(a, b, c, d, 4, 3, x) d = _f1(d, a, b, c, 5, 7, x) c = _f1(c, d, a, b, 6, 11, x) b = _f1(b, c, d, a, 7, 19, x) a = _f1(a, b, c, d, 8, 3, x) d = _f1(d, a, b, c, 9, 7, x) c = _f1(c, d, a, b, 10, 11, x) b = _f1(b, c, d, a, 11, 19, x) a = _f1(a, b, c, d, 12, 3, x) d = _f1(d, a, b, c, 13, 7, x) c = _f1(c, d, a, b, 14, 11, x) b = _f1(b, c, d, a, 15, 19, x) a = _f2(a, b, c, d, 0, 3, x) d = _f2(d, a, b, c, 4, 5, x) c = _f2(c, d, a, b, 8, 9, x) b = _f2(b, c, d, a, 12, 13, x) a = _f2(a, b, c, d, 1, 3, x) d = _f2(d, a, b, c, 5, 5, x) c = _f2(c, d, a, b, 9, 9, x) b = _f2(b, c, d, a, 13, 13, x) a = _f2(a, b, c, d, 2, 3, x) d = _f2(d, a, b, c, 6, 5, x) c = _f2(c, d, a, b, 10, 9, x) b = _f2(b, c, d, a, 14, 13, x) a = _f2(a, b, c, d, 3, 3, x) d = _f2(d, a, b, c, 7, 5, x) c = _f2(c, d, a, b, 11, 9, x) b = _f2(b, c, d, a, 15, 13, x) a = _f3(a, b, c, d, 0, 3, x) d = _f3(d, a, b, c, 8, 9, x) c = _f3(c, d, a, b, 4, 11, x) b = _f3(b, c, d, a, 12, 15, x) a = _f3(a, b, c, d, 2, 3, x) d = _f3(d, a, b, c, 10, 9, x) c = _f3(c, d, a, b, 6, 11, x) b = _f3(b, c, d, a, 14, 15, x) a = _f3(a, b, c, d, 1, 3, x) d = _f3(d, a, b, c, 9, 9, x) c = _f3(c, d, a, b, 5, 11, x) b = _f3(b, c, d, a, 13, 15, x) a = _f3(a, b, c, d, 3, 3, x) d = _f3(d, a, b, c, 11, 9, x) c = _f3(c, d, a, b, 7, 11, x) b = _f3(b, c, d, a, 15, 15, x) state[0] = (state[0] + a) & 0xffffffff state[1] = (state[1] + b) & 0xffffffff state[2] = (state[2] + c) & 0xffffffff state[3] = (state[3] + d) & 0xffffffff return state
def decrypt(ciphertext, padding_oracle=None, decryption_oracle=None, iv=None, block_size=16, is_correct=True, amount=0, known_plaintext=None, async_calls=False): """Decrypt ciphertext Give padding_oracle or decryption_oracle (or both) Args: ciphertext(string): to decrypt padding_oracle(function/None) decryption_oracle(function/None) iv(string): if not specified, first block of ciphertext is treated as iv block_size(int) is_correct(bool): set if ciphertext will decrypt to something with correct padding amount(int): how much blocks decrypt (counting from last), zero (default) means all known_plaintext(string): with padding, from end (aligned to end of ciphertext) async_calls(bool): make asynchronous calls to oracle (not implemented yet) Returns: plaintext(string): with padding """ _check_oracles(padding_oracle=padding_oracle, decryption_oracle=decryption_oracle, block_size=block_size) if block_size % 8 != 0: log.critical_error("Incorrect block size: {}".format(block_size)) if len(ciphertext) % block_size != 0: log.critical_error("Incorrect ciphertext length: {}".format( len(ciphertext))) if decryption_oracle: if iv: ciphertext = iv + ciphertext blocks = chunks(ciphertext, block_size) plaintext = bytes(b'') for position in range(len(blocks) - 1, 0, -1): plaintext = xor(decryption_oracle(blocks[position]), blocks[position - 1]) + plaintext log.info("Plaintext(hex): {}".format(b2h(plaintext))) if amount != 0 and len(plaintext) == amount * block_size: break log.success("Decrypted(hex): {}".format(b2h(plaintext))) return plaintext log.info("Start cbc padding oracle") log.debug(print_chunks(chunks(ciphertext, block_size))) # prepare blocks blocks = chunks(ciphertext, block_size) if iv: if len(iv) % block_size != 0: log.critical_error("Incorrect iv length: {}".format(len(iv))) log.info("Set iv") blocks.insert(0, iv) if amount != 0: amount = len(blocks) - amount - 1 if amount < 0 or amount >= len(blocks): log.critical_error( "Incorrect amount of blocks to decrypt: {} (have to be in [0,{}]". format(amount, len(blocks) - 1)) log.info("Will decrypt {} block(s)".format(len(blocks) - 1 - amount)) # add known plaintext plaintext = bytes(b'') position_known = 0 chars_decoded = 0 if known_plaintext: is_correct = False plaintext = known_plaintext blocks_decoded = len(plaintext) // block_size chars_decoded = len(plaintext) % block_size if blocks_decoded == len(blocks) - 1: log.debug("Nothing decrypted, known plaintext long enough") return plaintext if blocks_decoded > len(blocks) - 1: log.critical_error( "Too long known plaintext ({} blocks)".format(blocks_decoded)) if blocks_decoded != 0: blocks = blocks[:-blocks_decoded] position_known = chars_decoded log.info("Have known plaintext, skip {} block(s) and {} bytes".format( blocks_decoded, chars_decoded)) # start decryption for count_block in range(len(blocks) - 1, amount, -1): """ Blocks from the last to the second (all except iv) """ log.info("Block no. {}".format(count_block)) payload_prefix = bytes(b''.join(blocks[:count_block - 1])) payload_modify = blocks[count_block - 1] payload_decrypt = blocks[count_block] if chars_decoded != 0: # we know some chars, so modify previous block payload_modify = payload_modify[:-chars_decoded] +\ xor(plaintext[:chars_decoded], payload_modify[-chars_decoded:], bytes([chars_decoded + 1])) chars_decoded = 0 position = block_size - 1 - position_known position_known = 0 while position >= 0: """ Every position in block, from the end """ log.debug("Position: {}".format(position)) found_correct_char = False for guess_char in range(256): modified = payload_modify[:position] + bytes( [guess_char]) + payload_modify[position + 1:] payload = bytes(b''.join( [payload_prefix, modified, payload_decrypt])) iv = payload[:block_size] payload = payload[block_size:] log.debug(print_chunks(chunks(iv + payload, block_size))) correct = padding_oracle(payload=payload, iv=iv) if correct: """ oracle returns True """ padding = block_size - position # sent ciphertext decoded to that padding decrypted_char = bytes( [payload_modify[position] ^ guess_char ^ padding]) if is_correct: """ If we didn't send original ciphertext, then we have found original padding value. Otherwise keep searching and if won't find any other correct char - padding is \x01 """ if guess_char == blocks[-2][-1]: log.debug( "Skip this guess char ({})".format(guess_char)) continue dc = int(decrypted_char[0]) log.info( "Found padding value for correct ciphertext: {}". format(dc)) if dc == 0 or dc > block_size: log.critical_error( "Found bad padding value (given ciphertext may not be correct)" ) plaintext = decrypted_char * dc payload_modify = payload_modify[:-dc] + xor( payload_modify[-dc:], decrypted_char, bytes([dc + 1])) position = position - dc + 1 is_correct = False else: """ abcd efgh ijkl o|guess_char|xy || 1234 5678 9tre qwer - ciphertext what ever itma ybex || xyzw rtua lopo k|\x03|\x03\x03 - plaintext abcd efgh ijkl |guess_char|wxy || 1234 5678 9tre qwer - next round ciphertext some thin gels eheh || xyzw rtua lopo guessing|\x04\x04\x04 - next round plaintext """ if position == block_size - 1: """ if we decrypt first byte, check if we didn't hit other padding than \x01 """ payload = iv + payload payload = payload[:-block_size - 2] + bytes( b'A') + payload[-block_size - 1:] iv = payload[:block_size] payload = payload[block_size:] correct = padding_oracle(payload=payload, iv=iv) if not correct: log.debug("Hit false positive, guess char({})". format(guess_char)) continue payload_modify = payload_modify[:position] + xor( bytes([guess_char]) + payload_modify[position + 1:], bytes([padding]), bytes([padding + 1])) plaintext = decrypted_char + plaintext found_correct_char = True log.debug( "Guessed char(\\x{:02x}), decrypted char(\\x{:02x})". format(guess_char, decrypted_char[0])) log.debug("Plaintext: {}".format(plaintext)) log.info("Plaintext(hex): {}".format(b2h(plaintext))) break position -= 1 if found_correct_char is False: if is_correct: padding = 0x01 payload_modify = payload_modify[:position + 1] + xor( payload_modify[position + 1:], bytes([padding]), bytes([padding + 1])) plaintext = bytes(b"\x01") is_correct = False else: log.critical_error( "Can't find correct padding (oracle function return False 256 times)" ) log.success("Decrypted(hex): {}".format(b2h(plaintext))) return plaintext