def q3_realistic_aes_cache_attack( less_leaky_encipher=lab5_helper.less_leaky_encipher_example): """Question 3: Realistic cache timing attack on AES In this problem, you're still acting as Mallory and trying to perform a cache timing attack. There's just one new hurdle that you must overcome. (As a consequence: do not attempt to solve this problem until you have already solved Question 2.) I made one unrealistic assumption in the 'leaky_encipher' routine: I provided you with the set of bytes that were accessed in the final round of AES. Real caches unfortunately do not provide byte-level accuracy. I'll spare you the details; the upshot is that it is common 16 values of the SubBytes array to fit within a single cacheline. That is: suppose Bob weren't running AES at all, but instead only makes a single table lookup S[x] into the SubBytes array S. By observing which portion of the cache is activated, a cache attack would let Mallory know whether Bob's access x was in the range 0-15, or the range 16-31, or the range 32-47, ... or the range 240-255. However, Mallory couldn't tell anything beyond that. Put another way: Mallory can learn the upper 4 bits of x but not the lower 4 bits. The 'lab5_helper.py' file contains Bob's code for this problem. It is the routine less_leaky_encipher_example that only provides (the set of) the upper 4 bits of the location of each table lookup to Mallory; it otherwise runs similarly to the code in Question 2. Your Task: Perform a cache timing attack even in this restricted setting. Your input-output behavior should be the same as stated in Question 2. (The solution to this problem is pretty much exactly what Osvik, Shamir, and Tromer did to break Linux's full disk encryption software, called dmcrypt.) """ key = '' for index in range(0, 16): byteList = [] for i in range(0, 256): byteList.append(i) while (len(byteList) != 1): randomMessage = os.urandom(16) result = less_leaky_encipher(randomMessage) cipherText = result[0] hexCipher = cipherText.hex() cipher_int = [ int(hexCipher[i] + hexCipher[i + 1], 16) for i in range(0, len(hexCipher), 2) ] cache_line = list(result[1]) test = cipher_int[index] candidate = [] for x in byteList: temp = test ^ x val = lab5_helper.Sinv(temp) if val >> 4 in cache_line: candidate.append(x) byteList = candidate temp = hex(byteList[0]) temp = temp[2:] temp = temp.zfill(2) key += temp return key
def find_sinv(sinv_input): myList = [] outputList = [] for a in range(16): myList.append(16 * sinv_input + a) # print(myList) for x in range(256): res = lab5_helper.Sinv(x) for i in range(16): if res == myList[i]: outputList.append(hex(x)[2:].rjust(2, "0")) return outputList
def q3_realistic_aes_cache_attack(less_leaky_encipher=lab5_helper.less_leaky_encipher_example): """Realistic cache timing attack on AES""" def get_rand_string(perm_size): return ''.join(choices(printable, k=perm_size)) index=0 matrixP=[] last_round_key="" check=[0,0] intersection=[0,0,0] for i in range(0,31,2): matrixK=[] for s in range(40): index+=1 plaintext=get_rand_string(16) cipher,cache=less_leaky_encipher(plaintext.encode()) lists=[] hexC=binascii.hexlify(cipher) firstByte=hexC[i:i+2] poss=[] for j in range(256): key="{:02x}".format(j) xor=int(strxor(binascii.unhexlify(firstByte),binascii.unhexlify(key)).hex(),16) b=lab5_helper.Sinv(xor) poss.append(b) key_poss=[] for k,p in enumerate(poss): if p>>4 in cache: key_poss.append(k) matrixK.append(key_poss) matrixP.append(poss) temp=set(matrixK[0]) for m in range(len(matrixK)): temp=temp&set(matrixK[m]) ans=list(temp) if len(ans)!=1: print("not done") break else: res=hex(ans[0])[2:] if len(res) ==1: res='0'+res last_round_key+=res return last_round_key
def find_sinv(sinv_input): for x in range(256): res = lab5_helper.Sinv(x) if res == sinv_input: return hex(x)[2:].rjust(2, "0")
def q2_simple_aes_cache_attack( leaky_encipher=lab5_helper.leaky_encipher_example): """Question 2: Simple cache timing attack on AES As Mallory, you must determine the last round key at the very end of AES. Since you are a legitimate user on the machine, you're welcome to encipher files whenever you'd like, and you can also introspect the state of the cache using techniques like Prime+Probe that we discussed in class. Bob's code for file enciphering is provided as the 'leaky_encipher' routine passed to this function (Note: you can find an example of the 'leaky_encipher' routine in 'lab5_helper.py'). Bob's routine does both of the above operations for you: it enciphers a file and then helpfully tells you how the 10th round S-box lookups have influenced the state of the cache, so you don't need to inspect it yourself. Hence, 'leaky_encipher' has two outputs: the actual ciphertext plus a Python set stating which cachelines are accessed during the final round's SubBytes operation. Recall that SubBytes works on a byte-by-byte basis: each byte of the state is used to fetch a specific location within the S-box array. The 'leaky_encipher' routine tells you which elements of the S-box array were accessed, which as you recall from Lecture 10 is correlated with the key. I'll state two caveats upfront: - This problem conducts a last-round attack: that is, our attack scenario is explained in lecture 10 slides As a result, the cache lines are correlated with the last round key of AES, and not the first round key. This is acceptable to Mallory because there's a known, public permutation that relates all of the round keys. In fact in my helper file 'aeskeyexp.py' I have provided a routine 'aes128_lastroundkey' that converts first -> last round keys. I didn't actually give you the converse, but I assure you that it's equally as easy to compute. Let's just declare victory as Mallory if we can find the last round key. - Mallory cannot interrupt the state of execution of AES. She can only observe the contents of the cache after it is finished. As a result: leaky_encipher only tells you the **set** of all table lookups made to the 10th round S-box across all 16 bytes, without telling you which lookup is associated with which byte. Your Task: Complete this function with a solution that calls 'leaky_encipher' as many times as you wish and uses the results to determine the key. Args: leaky_encipher (func) : performs an AES encipher on the input 16-bytes input `file_bytes` Args: file_bytes (bytes) : 16-bytes input to be passed to AES for enciphering Output: ret (str, set) : tuple with the actual ciphertext and a Python set stating which cachelines are accessed during the final round's SubBytes operation. Output: ret (str) : hex-encoded 16-bytes string that represents the lastroundkey of AES in leaky_encipher How to verify your answer: assert(q2_simple_aes_cache_attack() == aeskeyexp.aes128_lastroundkey(lab5_helper.TEST_KEY).hex()) Note: The file `lab5_helper.py` contains some helper functions that you find useful in solving this question. """ key = '' for index in range(0, 16): byteList = [] for i in range(0, 256): byteList.append(i) while (len(byteList) != 1): randomMessage = os.urandom(16) result = leaky_encipher(randomMessage) cipherText = result[0] hexCipher = cipherText.hex() cipher_int = [ int(hexCipher[i] + hexCipher[i + 1], 16) for i in range(0, len(hexCipher), 2) ] cache_line = list(result[1]) test = cipher_int[index] candidate = [] for x in byteList: temp = test ^ x val = lab5_helper.Sinv(temp) if val in cache_line: candidate.append(x) byteList = candidate temp = hex(byteList[0]) temp = temp[2:] temp = temp.zfill(2) key += temp return key