ØxOPOSɆC Crypto Challenges 2019

Write-up for ØxOPOSɆC crypto challenge that involves AES in CTR mode.

ØxOPOSɆC Crypto Challenges 2019

Level 1

This first level is an example of how the known-plaintext attack can be used against the AES-CTR algorithm, if incorrectly implemented.

We're given a ciphered text (C1):

5/KIDoW4XvtTCLZQSQl9GYBmdSC+vj2wDxP4Bbg=

and its plaintext equivalent (P1):

this data is indeed important

And we're asked to decipher a second cipher (C2):

9faAGt62D79tSa1GNhR2FbpgY3mnpyKAEVfrD7E=

We also know that this is an implementation of the AES block cipher in CTR mode, and that both plaintexts were encrypted with the same key and IV.

In these conditions we know that:

C1 XOR C2 = P1 XOR P2

and as such:

P2 = P1 XOR (C1 XOR C2)

To find P2 I created a simple Python script:

import argparse
import base64
def main(c1, c2, p1):
decoded_c1 = base64.b64decode(c1)
c1_byte_arr = bytearray(decoded_c1)
decoded_c2 = base64.b64decode(c2)
c2_byte_arr = bytearray(decoded_c2)
p1_byte_arr = bytearray(p1.encode("ascii"))
c1_xor_c2 = []
p2 = []
for i in range(len(c1_byte_arr)):
xored = c1_byte_arr[i] ^ c2_byte_arr[i]
c1_xor_c2.append(xored)
for j in range(len(c1_xor_c2)):
xored2 = p1_byte_arr[j] ^ c1_xor_c2[j]
p2.append(xored2)
print("".join(map(chr, p2)))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Parses keys from USB keyboard capture file')
parser.add_argument('-c1',
dest='c1',
help='Base64-encoded ciphertext with known plaintext',
type=str,
required=True)
parser.add_argument('-c2',
dest='c2',
help='Base64-encoded ciphertext with unknown plaintext',
type=str,
required=True)
parser.add_argument('-p1',
dest='p1',
help='Known plaintext',
type=str,
required=True)
args = parser.parse_args()
main(args.c1, args.c2, args.p1)
# Example:
# python3 test.py -c1 "5/KIDoW4XvtTCLZQSQl9GYBmdSC+vj2wDxP4Bbg=" -c2 "9faAGt62D79tSa1GNhR2FbpgY3mnpyKAEVfrD7E=" -p1 "this data is indeed important"

Level 2

On this level, we're  provided with an example of a valid cipher but not its plaintext equivalent:

22jBDaecKR9Sn4JXzJkxXwY/cAgC9/Gg5JImfEgQLC0=

We also have access to a server which confirms or denies the validity of any given cipher, but it also lets us know when the error is due to invalid padding, which makes it vulnerable to padding oracle attacks.

Instead of creating a "new" Python script, I simply adapted the following PoC: https://github.com/mpgn/Padding-oracle-attack:

import os
import sys
url = [SERVER_URL]
flag_hex = [KNOWN_VALID_FLAG_IN_HEX]
base64_error = [INVALID_BASE64_ERROR_MSG]
len_error = [INVALID_LENGTH_ERROR_MSG]
padding_error = [WRONG_PADDING_ERROR_MSG]
success_msg = [SUCCESS_MSG]
import argparse
import httplib, urllib
import re
import binascii
import os
import sys
import time
from binascii import unhexlify, hexlify
from itertools import cycle, izip
def call_oracle(hex_cipher):
unencoded = hex_cipher.decode("hex")
test_flag = unencoded.encode("base64")
request = "curl -X POST -F 'encrypted=" + test_flag + "' --url " + url
print(request)
result = os.popen(request).read()
if padding_error in str(result) or len_error in str(result): #or base64_error in str(result)
return 0
else:
return 1
# the exploit don't need to touch this part
# split the cipher in len of size_block
def split_len(seq, length):
return [seq[i:i+length] for i in range(0, len(seq), length)]
''' create custom block for the byte we search'''
def block_search_byte(size_block, i, pos, l):
hex_char = hex(pos).split('0x')[1]
return "00"*(size_block-(i+1)) + ("0" if len(hex_char)%2 != 0 else '') + hex_char + ''.join(l)
''' create custom block for the padding'''
def block_padding(size_block, i):
l = []
for t in range(0,i+1):
l.append(("0" if len(hex(i+1).split('0x')[1])%2 != 0 else '') + (hex(i+1).split('0x')[1]))
return "00"*(size_block-(i+1)) + ''.join(l)
def hex_xor(s1,s2):
return hexlify(''.join(chr(ord(c1) ^ ord(c2)) for c1, c2 in zip(unhexlify(s1), cycle(unhexlify(s2)))))
def run():
cipher = flag_hex
size_block = 16
found = False
valide_value = []
result = []
len_block = size_block*2
cipher_block = split_len(cipher, len_block)
if len(cipher_block) == 1:
print "[-] Abort there is only one block"
sys.exit()
#for each cipher_block
for block in reversed(range(1,len(cipher_block))):
if len(cipher_block[block]) != len_block:
print "[-] Abort length block doesn't match the size_block"
break
print "[+] Search value block : ", block, "\n"
#for each byte of the block
for i in range(0,size_block):
# test each byte max 255
for ct_pos in range(0,256):
# 1 xor 1 = 0 or valide padding need to be checked
if ct_pos != i+1 or (len(valide_value) > 0 and int(valide_value[-1],16) == ct_pos):
bk = block_search_byte(size_block, i, ct_pos, valide_value)
bp = cipher_block[block-1]
bc = block_padding(size_block, i)
tmp = hex_xor(bk,bp)
cb = hex_xor(tmp,bc).upper()
up_cipher = cb + cipher_block[block]
#time.sleep(0.5)
# we call the oracle, our god
valid = call_oracle(up_cipher)
if valid == 1:
found = True
# data analyse and insert in rigth order
value = re.findall('..',bk)
valide_value.insert(0,value[size_block-(i+1)])
bytes_found = ''.join(valide_value)
if i == 0 and bytes_found.decode("hex") > hex(size_block) and block == len(cipher_block)-1:
print "[-] Error decryption failed the padding is > "+str(size_block)
sys.exit()
print '\033[36m' + '\033[1m' + "[+]" + '\033[0m' + " Found", i+1, "bytes :", bytes_found
print ''
break
if found == False:
# lets say padding is 01 for the last byte of the last block (the padding block)
if len(cipher_block)-1 == block and i == 0:
value = re.findall('..',bk)
valide_value.insert(0,"01")
else:
print "\n[-] Error decryption failed"
result.insert(0, ''.join(valide_value))
hex_r = ''.join(result)
print "[+] Partial Decrypted value (HEX):", hex_r.upper()
padding = int(hex_r[len(hex_r)-2:len(hex_r)],16)
print "[+] Partial Decrypted value (ASCII):", hex_r[0:-(padding*2)].decode("hex")
sys.exit()
found = False
result.insert(0, ''.join(valide_value))
valide_value = []
print ''
hex_r = ''.join(result)
print "[+] Decrypted value (HEX):", hex_r.upper()
padding = int(hex_r[len(hex_r)-2:len(hex_r)],16)
print "[+] Decrypted value (ASCII):", hex_r[0:-(padding*2)].decode("hex")
if __name__ == '__main__':
run()