def AES128_CBC_decrypt(ciphertext, key): blocks = list(grouper(BLOCKSIZE, ciphertext)) # Initialization vector last_cipherblock = blocks[0] plaintext = bytearray() for block in blocks[1:]: block = bytes(block) decrypt = AES128_decrypt(block, key) decrypt = xorvec(last_cipherblock, decrypt) plaintext.extend(decrypt) last_cipherblock = block return plaintext
def AES128_CBC_encrypt(plaintext, key, iv=None): if iv is None: iv = os.urandom(BLOCKSIZE) last_cipherblock = iv ciphertext = bytearray(iv) for block in grouper(BLOCKSIZE, plaintext): block = bytes(block) encrypt = AES128_encrypt( xorvec(last_cipherblock, block), key) ciphertext.extend(encrypt) last_cipherblock = encrypt return ciphertext
def crack_constant_ctr_nonce(ciphertexts): cx_metric = english_letters_metric # Put all the first (second, third, etc.) bytes of the ciphertexts # togther into a single "message" that we'll run XOR-cipher # cracking on. Just skip ciphertexts that are too short to # contribute a byte to the i'th message. messages = [] decrypts = [] for i in range(max(len(m) for m in ciphertexts)): message = bytearray() for m in ciphertexts: if i >= len(m): continue message.append(m[i]) messages.append(message) decrypts.append(crack_xorchar(message, metric=cx_metric)) # For most key bytes, there are multiple keys that score the same # on the english_letters_metric. Use a "maximize printable # characters" metric to select which to use (as it turns out, this # just renders it more readable without significantly reducing the # work required to clean up the decryption by hand, but I'll take # what I can get). key = bytearray() for i in range(len(decrypts)): # print(i) decryptset = decrypts[i] position = [] top = cx_metric(decryptset[0][1]) for k, t in decryptset: if cx_metric(t) == top: position.append((k, t)) position.sort(key=lambda t: printable_metric(t[1])) position.reverse() # for k, t in position: # print(k, printable_metric(t), t) key.append(position[0][0]) # Start decrypting! ret = [] for m in ciphertexts: plaintext = xorvec(key[:len(m)], m) #print(m, '==>', plaintext) ret.append(plaintext) return ret
def run_p16(): print('Problem 16') # Get a cookie using some random data. cookie = p16_cookie(b'ohai') assert(not p16_cookie_is_admin(cookie)) # The second block of the cookie encodes the first block of the # prefix. Since we know that, we can modify the IV (the first # block of the cookie) such that the second block will decode to # what we want. new_first_block_plaintext = b';admin=true;' new_first_block_plaintext += bytes((-len(new_first_block_plaintext)) % BLOCKSIZE) old_iv = cookie[:BLOCKSIZE] new_iv = xorvec(old_iv, P16_PREFIX[:BLOCKSIZE], new_first_block_plaintext) new_cookie = new_iv + cookie[BLOCKSIZE:] if p16_cookie_is_admin(new_cookie): print('We made an admin cookie!') else: print('Our best attempts at forgery were insufficient. We made this cookie:') print(p16_cookie_decode(new_cookie))
def crack_helper(index, known_part): # index: character that we're trying to deduce # known_part: the last (blocksize - index - 1) bytes of the block # plaintext, which we should know at this point. # If we are trying to crack the 2nd byte of 16, we want 15 # bytes of padding. The i'th byte corresponds to # index = (i - 1), and we happen to want # BLOCKSIZE - (i - 1) pad bytes. Very convenient. pad_char = BLOCKSIZE - index tamper_suffix_len = pad_char - 1 assert(tamper_suffix_len + index + 1 == BLOCKSIZE) # Create a suffix to the tamper IV such that AES_decrypt(block) XOR # prev_block XOR tamper_iv will have a suffix of pad_char. # # K = padding character # D[i] = AES_decrypt(block)[i] # V[i] = prev_block[i] # P[i] = known_part[i] # T[i] = tamper_iv[i] # # We know that P[i] = V[i] ^ D[i]. We want D[i] ^ T[i] = K. # T[i] = K ^ D[i] # D[i] = P[i] ^ V[i] (yes, we could theoretically keep this around # instead of recomputing it every time) # T[i] = K ^ P[i] ^ V[i] pad_suffix = bytes([pad_char] * tamper_suffix_len) if tamper_suffix_len > 0: tamper_iv_suffix = xorvec(pad_suffix, known_part[-tamper_suffix_len:], prev_block[-tamper_suffix_len:]) else: tamper_iv_suffix = bytes() assert(len(tamper_iv_suffix) == pad_char - 1) # OK, now we look for tamper_iv[index] (let's call that 'T') # such that # # (tamper_iv^aes_decrypt(block))[index] == pad_char. # # We do that by finding a tamper_iv that results in # tamper_iv^aes_decrypt(block) being a block with valid # padding, and we do that using the padding oracle. There's a # wrinkle here, though; there are sometimes *two* # candidates. Consider the case where the target plaintext # ends with '\x02\x55'. Replacing 0x55 by either 0x01 *or* # 0x02 will produce a correctly padded block. However, the # tamper_iv[index] that produced 0x01 will (probably) at some # point lead us down a blind alley where we can not find the # next tamper_iv[index], and, at that point, we backtrack. # # TODO: eliminate the probabilistic element from the above. candidates = [] for T in range(256): tamper_iv = zero_prefix(bytes([T]) + tamper_iv_suffix, BLOCKSIZE) if oracle(tamper_iv + block): candidates.append(T) # Now, remember, for each valid T, # T ^ AES_decrypt(block)[index] = pad_char # We want to recover the *actual* plaintext, which is: # plaintext_byte = prev_block[index] ^ AES_decrypt(block)[index] # = prev_block[index] ^ T ^ pad_char candidates = [prev_block[index] ^ T ^ pad_char for T in candidates] return [bytes([c]) + known_part for c in candidates]