def guess_ecb_cbc(nb_repeating_blocks, nb_runs, url): result = { 'total_oracle_calls': 0, 'step1': {}, 'step2': {}, 'step3': {}, } # step 1: detect block size block_byte_size, nb_oracle_calls = query_oracle.get_block_size(url) result['step1'] = { 'nb_oracle_calls': nb_oracle_calls, 'block_byte_size': block_byte_size, } result['total_oracle_calls'] += nb_oracle_calls # step 2: create a repeating plain base_bytes = bytes([00]) base_bytes += b'A' * (block_byte_size - 1) plain_bytes = base_bytes * nb_repeating_blocks result['step2'] = { 'nb_repeating_blocks': nb_repeating_blocks, 'base_plain': encoder.bytes_to_hex(base_bytes), } # step 3: query the oracle with that special plain and analyze the cipher result['step3'] = { 'nb_oracle_calls': 0, 'attempts': [], } for i in range(0, nb_runs): attempt = { 'sent': encoder.bytes_to_hex(plain_bytes), 'received': None, 'guessed': None, } cipher_bytes = query_oracle.get_response(url, plain_bytes) attempt['received'] = encoder.bytes_to_hex(cipher_bytes) result['step3']['nb_oracle_calls'] += 1 blocks = [ cipher_bytes[i:i + block_byte_size] for i in range(0, len(cipher_bytes), block_byte_size) ] duplicates = list( set(block for block in blocks if blocks.count(block) > 1)) # plain with identical blocks and cipher too: it is ecb if len(duplicates) > 0: attempt['guessed'] = 'ecb' else: attempt['guessed'] = 'cbc' result['step3']['attempts'].append(attempt) result['total_oracle_calls'] += result['step3']['nb_oracle_calls'] return result
def do_GET(self): data = self._parse_query_string().get('data', None) user_hex = '' if data is not None: user_hex = data[0] user_bytes = self._convert_or_raise_input(user_hex) # generate key and iv key = randomness.get_bytes(self.__KEY_LENGTH) iv = randomness.get_bytes(16) # pick ECB or CBC chaining mode randomly flip = bool(randomness.get_bits(1)) # encrypt cipher_bytes = None if flip is True: cipher_bytes = aes.encrypt(user_bytes, key, 'ecb') else: cipher_bytes = aes.encrypt(user_bytes, key, 'cbc', iv) # remove iv cipher_bytes = cipher_bytes[16:] cipher_hex = encoder.bytes_to_hex(cipher_bytes) # send response self._send_response(cipher_hex)
def do_POST(self): content = self._parse_body() data = content.get(b'data', None) if data is None: self.send_error(400, 'received empty encrypted comment') encrypted_comment = encoder.hex_to_bytes(data[0].decode('utf-8')) iv = encrypted_comment[:16] try: encoded_bytes = aes.decrypt(encrypted_comment[16:], self.__KEY, 'cbc', iv) except Exception as err: self.send_error(400, str(err)) encoded_hex = encoder.bytes_to_hex(encoded_bytes) self._send_response(encoded_hex)
def do_GET(self): data = self._parse_query_string().get('data', None) user_hex = '' if data is not None: user_hex = data[0] user_bytes = self._convert_or_raise_input(user_hex) # append secret plain_bytes = before + user_bytes + self.__SECRET # encrypt cipher_bytes = aes.encrypt(plain_bytes, self.__KEY, 'ecb') cipher_hex = encoder.bytes_to_hex(cipher_bytes) # send response self._send_response(cipher_hex)
def get_response(url, data: bytes, verb='GET'): data_hex = encoder.bytes_to_hex(data) response = None if verb == 'GET': response = requests.get(url, params={'data': data_hex}) if verb == 'POST': response = requests.post(url, data={'data': data_hex}) response.encoding = 'utf-8' result = b'' if response.status_code == '200': result = encoder.hex_to_bytes(response.text) else: result = encoder.utf8_to_bytes(response.reason) return encoder.hex_to_bytes(response.text)
def do_GET(self): data = self._parse_query_string().get('data', None) data_hex = '' if data is not None: data_hex = data[0] data_utf8 = encoder.hex_to_utf8(data_hex) # create comment, encode then encrypt comment = self.__create_comment(data_utf8) encoded = self.__encode_comment(comment) encoded_bytes = encoder.utf8_to_bytes(encoded) iv = randomness.get_bytes(16) cipher_bytes = aes.encrypt(encoded_bytes, self.__KEY, 'cbc', iv) # remove iv cipher_hex = encoder.bytes_to_hex(cipher_bytes) self._send_response(cipher_hex)
def attack_ecb(url): result = { 'total_oracle_calls': 0, 'step1': {}, 'step2': {}, 'step3': {}, 'step4': {}, } # step 1: detect block size block_byte_size, nb_oracle_calls = query_oracle.get_block_size(url) result['step1'] = { 'nb_oracle_calls': nb_oracle_calls, 'block_byte_size': block_byte_size, } result['total_oracle_calls'] += nb_oracle_calls # step 2: verify chaining mode is ECB is_ecb, nb_oracle_calls = query_oracle.is_ecb_mode(url, block_byte_size) if is_ecb is False: raise RuntimeError('The oracle is not chaining blocks in ECB mode. Aborting.') result['step2'] = { 'nb_oracle_calls': nb_oracle_calls, 'verified_ecb': True, } result['total_oracle_calls'] += nb_oracle_calls # step 3: detect the offset from which we fully control encrypted blocks first_controlled_block_index, prefix_length, nb_oracle_calls = query_oracle.get_offset_first_controlled_block(url, block_byte_size) prefix_bytes = bytes([0]) * prefix_length result['step3'] = { 'nb_oracle_calls': nb_oracle_calls, 'first_controlled_block_index': first_controlled_block_index, 'chosen_plaintext_prefix': encoder.bytes_to_hex(prefix_bytes), } result['total_oracle_calls'] += nb_oracle_calls # step 4: extract secret byte per byte result['step4'], nb_oracle_calls = query_oracle.bruteforce_ecb(url, block_byte_size, first_controlled_block_index, prefix_bytes) result['step4']['nb_oracle_calls'] = nb_oracle_calls result['total_oracle_calls'] += nb_oracle_calls return result
def bruteforce_ecb(url, block_byte_size: int, first_controlled_block_index: int, prefix: bytes): nb_oracle_calls = 0 result = { 'witnesses': { 'collected': False, 'nb_oracle_calls': 0, }, 'bruteforce': [], 'secret': { 'length': 0, 'nb_oracle_calls': 0, 'value': None, }, } # store encryptions of [s(0)/s(b)/s(2b)/..., s(1)/s(b+1)/s(2b+1)/..., ...] witnesses = [] for offset in range(0, block_byte_size): base_block = bytes([0]) * (block_byte_size - (offset + 1)) base_block = prefix + base_block witness = get_response(url, base_block) result['witnesses']['nb_oracle_calls'] += 1 witnesses.append(witness) result['witnesses']['collected'] = True nb_oracle_calls += result['witnesses']['nb_oracle_calls'] # get length of the secret # thanks to pkcs7: full block of padding is added if plaintext is a multiple of block_byte_size initial_length = len(get_response(url, prefix)) result['secret']['nb_oracle_calls'] += 1 for i in range(1, block_byte_size): plain_bytes = prefix + bytes([0]) * i length = len(get_response(url, plain_bytes)) result['secret']['nb_oracle_calls'] += 1 if length > initial_length: result['secret'][ 'length'] = initial_length - block_byte_size * first_controlled_block_index - i break nb_oracle_calls += result['secret']['nb_oracle_calls'] # attack the secret block per block (s(0)..s(b-1), then s(b)..s(2b-1), etc) secret = b'' block_index = first_controlled_block_index while len(secret) < result['secret']['length']: block_start = block_byte_size * block_index block_end = block_start + block_byte_size # bruteforce s(offset) inside the current block for offset in range(0, block_byte_size): attempt = { 'block_index': block_index, 'witness': None, 'sent': None, 'received': None, 'nb_oracle_calls': 0, } # get witness block block_witness = witnesses[offset][block_start:block_end] attempt['witness'] = encoder.bytes_to_hex(block_witness) # build base block base_block = bytes([0]) * (block_byte_size - (offset + 1)) base_block = prefix + base_block + secret # bruteforce s(offset): query oracle for all possible values until getting the witness for guess in range(0, 256): plain_bytes = base_block + encoder.int_to_bytes(guess) cipher_bytes = get_response(url, plain_bytes) attempt['nb_oracle_calls'] += 1 block = cipher_bytes[block_start:block_end] if block == block_witness: secret += encoder.int_to_bytes(guess) attempt['sent'] = encoder.bytes_to_hex(plain_bytes) attempt['received'] = encoder.bytes_to_hex(cipher_bytes) result['bruteforce'].append(attempt) nb_oracle_calls += attempt['nb_oracle_calls'] print('*', end='', flush=True) break if len(secret) == result['secret']['length']: break block_index += 1 print('') result['secret']['value'] = secret return result, nb_oracle_calls
str_hex = '49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d' str_b64_expected = 'SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t' str_b64_observed = encoder.hex_to_b64(str_hex) assert str_b64_observed == str_b64_expected, 'Failed 1: Expected {}, got {}'.format( str_b64_expected, str_b64_observed) print(' ok') # Chall2 print('\n[+] Chall 2 (xor operation) ...', end='') a_hex = '1c0111001f010100061a024b53535009181c' b_hex = '686974207468652062756c6c277320657965' a_bytes = encoder.hex_to_bytes(a_hex) b_bytes = encoder.hex_to_bytes(b_hex) xor_bytes = bitwise.xor(a_bytes, b_bytes) xor_hex_expected = '746865206b696420646f6e277420706c6179' xor_hex_observed = encoder.bytes_to_hex(xor_bytes) assert xor_hex_observed == xor_hex_expected, 'Failed 2: Expected {}, got {}'.format( xor_hex_expected, xor_hex_observed) print(' ok') # Chall3 print('\n[+] Chall 3 (single byte xor cipher, frequency attack) ...', end='') cipher_hex = '1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736' cipher_bytes = encoder.hex_to_bytes(cipher_hex) best_key, best_plain, _ = xor_frequency.attack_single_byte( cipher_bytes, 'english') print(' ok: found key=0x{}, plain={}'.format( encoder.bytes_to_hex(best_key), encoder.bytes_to_utf8(best_plain))) # Chall4 print('\n[+] Chall 4 (detect single xor cipher, frequency attack) ...', end='')
def create_admin_profile_ecb(url, role_user: str, role_target: str): result = { 'total_oracle_calls': 0, 'step1': {}, 'step2': {}, 'step3': {}, } # step 1: detect block size block_byte_size, nb_oracle_calls = query_oracle.get_block_size(url) result['step1'] = { 'nb_oracle_calls': nb_oracle_calls, 'block_byte_size': block_byte_size, } result['total_oracle_calls'] += nb_oracle_calls # step 2: verify chaining mode is ECB is_ecb, nb_oracle_calls = query_oracle.is_ecb_mode(url, block_byte_size) if is_ecb is False: raise RuntimeError( 'The oracle is not chaining blocks in ECB mode. Aborting.') result['step2'] = { 'nb_oracle_calls': nb_oracle_calls, 'verified_ecb': True, } result['total_oracle_calls'] += nb_oracle_calls # step 3: forge an admin profile result['step3'] = { 'nb_oracle_calls': 0, } # request a decryption example to know the structure email_bytes = b'*****@*****.**' profile_encrypted = query_oracle.get_response(url, email_bytes) profile_bytes = query_oracle.get_response(url, profile_encrypted, 'POST') result['step3']['nb_oracle_calls'] += 2 result['step3']['sample'] = { 'email': encoder.bytes_to_utf8(email_bytes), 'profile': encoder.bytes_to_utf8(profile_bytes), 'encrypted': encoder.bytes_to_hex(profile_encrypted), } # choose an email so that low_privileged role (eg. 'user') get encrypted in a new block # let profile = before_email|email|before_role|user # block0 |block1 |block2 # before_email|emaiiii|iiiiiiil|before_role|user before_email, after_email = profile_bytes.split(email_bytes, 1) before_role = after_email.split(encoder.utf8_to_bytes(role_user), 1)[0] email_length = block_byte_size - (len(before_email) + len(before_role)) % block_byte_size domain = b'@example.com' while email_length < min(len(domain), block_byte_size): email_length += block_byte_size username = b'a' * (email_length - len(domain)) email_bytes = username + domain nb_blocks_to_keep = (len(before_email) + len(email_bytes) + len(before_role)) // block_byte_size profile_encrypted = query_oracle.get_response(url, email_bytes) result['step3']['nb_oracle_calls'] += 1 forged = profile_encrypted[:block_byte_size * nb_blocks_to_keep] result['step3']['pushed_user_out'] = { 'email': encoder.bytes_to_utf8(email_bytes), 'encrypted': encoder.bytes_to_hex(profile_encrypted), 'keeping': encoder.bytes_to_hex(forged), } # leveraging pkcs7 padding, get encryption of 'admin' alone # block0 |block1 |block2 # before_email|prefix |admin |after_email prefix = b'a' * (block_byte_size - len(before_email) % block_byte_size) email_bytes = padding.pad_pkcs7(encoder.utf8_to_bytes(role_target), block_byte_size) email_bytes = prefix + email_bytes profile_encrypted = query_oracle.get_response(url, email_bytes) result['step3']['nb_oracle_calls'] += 1 nb_block_to_skip = (len(before_email) + len(prefix)) // block_byte_size block_start = block_byte_size * nb_block_to_skip block_end = block_start + block_byte_size forged += profile_encrypted[block_start:block_end] result['step3']['admin_encryption'] = { 'prefix': encoder.bytes_to_utf8(prefix), 'admin_block_index': nb_block_to_skip, 'email': email_bytes, 'encrypted': encoder.bytes_to_hex(profile_encrypted), 'keeping': ' ' * block_byte_size * nb_block_to_skip * 2 + encoder.bytes_to_hex(profile_encrypted[block_start:block_end]), } # verify the forged encrypted profile profile = query_oracle.get_response(url, forged, 'POST') result['step3']['nb_oracle_calls'] += 1 result['step3']['forged'] = { 'encrypted': encoder.bytes_to_hex(forged), 'profile': encoder.bytes_to_utf8(profile), } result['total_oracle_calls'] += result['step3']['nb_oracle_calls'] return result
def add_admin_cbc(url, target_data: str): result = { 'total_oracle_calls': 0, 'step1': {}, 'step2': {}, 'step3': {}, } # step 1: detect block size block_byte_size, nb_oracle_calls = query_oracle.get_block_size(url) result['step1'] = { 'nb_oracle_calls': nb_oracle_calls, 'block_byte_size': block_byte_size, } result['total_oracle_calls'] += nb_oracle_calls if len(target_data) > block_byte_size: raise ValueError( 'Target data too long. Should be at most {} bytes'.format( block_byte_size)) # step 2: get a decryption sample to know the structure result['step2'] = { 'nb_oracle_calls': 0, } # encrypt/decrypt a sample sample_bytes = b'foobar' + encoder.utf8_to_bytes(target_data) comment_encrypted = query_oracle.get_response(url, sample_bytes) result['step2']['nb_oracle_calls'] += 1 comment_bytes = query_oracle.get_response(url, comment_encrypted, 'POST') result['step2']['nb_oracle_calls'] += 1 result['step2'].update({ 'sample': encoder.bytes_to_utf8(sample_bytes), 'comment': encoder.bytes_to_utf8(comment_bytes), 'encrypted': encoder.bytes_to_hex(comment_encrypted[16:]), }) result['total_oracle_calls'] += result['step2']['nb_oracle_calls'] # find prefix to ensure we know where we will inject our target data # +1 because of iv sample_quoted = quote(encoder.bytes_to_utf8(sample_bytes)) before_user_input = comment_bytes.split( encoder.utf8_to_bytes(sample_quoted), 1)[0] prefix = b'' if len(before_user_input) % block_byte_size != 0: prefix = b'x' * (block_byte_size - len(before_user_input) % block_byte_size) first_controlled_block_index = (len(before_user_input) + len(prefix)) // block_byte_size + 1 result['step2'].update({ 'chosen_plaintext_prefix': encoder.bytes_to_hex(prefix), 'first_controlled_block_index': first_controlled_block_index, }) # step 3: inject data to add data (eg. 'admin=true' or alike) result['step3'] = { 'nb_oracle_calls': 0, } # get a cipher for a string as long as the target we want witness = b'a' * len(target_data) ref_bytes = prefix + witness comment_encrypted = query_oracle.get_response(url, ref_bytes) comment_bytes = query_oracle.get_response(url, comment_encrypted, 'POST') result['step3']['nb_oracle_calls'] += 2 result['step3']['reference'] = { 'sent': encoder.bytes_to_utf8(ref_bytes), 'received': encoder.bytes_to_hex(comment_encrypted), 'comment': encoder.bytes_to_utf8(comment_bytes), } # leverage CBC chaining mode # starting from the global bloc matching cipher/plain: # |cipher0 |cipher1 |cipher2 # |before_user_input|prefix|000000000 |after_user_input # when decrypting: # |before_user_input|prefix|000000000 |after_user_input # iv |plain0 |plain1 |plain2 # |dec(cipher0) xor iv |dec(cipher1) xor cipher0|dec(cipher2) xor cipher1 # because xor is a bitwise operation, wisely tampering cipher0 (cipher0') will force the oracle to decrypt plain1 to the thing we want (plain1') # of course, plain0 will become gribbish # we know: cipher0, plain1 # plain1 = 00000000000 | after_user_input # plain1' = target_data | after_user_input # plain1 = dec(cipher1) xor cipher0 <=> dec(cipher1) = plain1 xor cipher0 # plain1' = dec(cipher1) xor cipher0' <=> cipher0' = plain1' xor dec(cipher1) # => cipher0' = plain1' xor plain1 xor cipher0 # note: because after_user_input xor after_user_input = 0, we will just pad plain1 and plain1' with 0x00 directly block_start = block_byte_size * (first_controlled_block_index - 1) block_end = block_start + block_byte_size cipher0 = comment_encrypted[block_start:block_end] plain1 = witness + bytes([0]) * (block_byte_size - len(target_data)) plain1prime = encoder.utf8_to_bytes(target_data) + bytes( [0]) * (block_byte_size - len(target_data)) cipher0prime = bitwise.xor(plain1, plain1prime) cipher0prime = bitwise.xor(cipher0prime, cipher0) forged = comment_encrypted[:block_start] + cipher0prime + comment_encrypted[ block_end:] comment_bytes = query_oracle.get_response(url, forged, 'POST') result['step3']['forging'] = { 'plain_block_target_index': first_controlled_block_index, 'cipher_block_to_tamper': encoder.bytes_to_hex(cipher0), 'plain_current': encoder.bytes_to_hex(plain1), 'plain_target': encoder.bytes_to_hex(plain1prime), 'cipher_block_tampered': encoder.bytes_to_hex(cipher0prime), 'sent': encoder.bytes_to_hex(forged), 'comment': comment_bytes, } result['step3']['nb_oracle_calls'] += 1 result['total_oracle_calls'] += result['step3']['nb_oracle_calls'] return result