leakycrypto: add gen.py and write up about hulk usage, reformat
This commit is contained in:
parent
8db550d150
commit
4b831bb439
|
@ -1,70 +1,38 @@
|
|||
## Leaky Crypto
|
||||
# Leaky Crypto
|
||||
|
||||
**Category**: Payload Modules
|
||||
**Points (final)**: 223 points
|
||||
**Solves**: 11
|
||||
|
||||
> My crypto algorithm runs in constant time, so I'm safe from sidechannel leaks, right?
|
||||
>
|
||||
> Note: To clarify, the sample data is plaintext inputs, NOT ciphertext
|
||||
|
||||
**Given files**: `leaky-romeo86051romeo.tar.bz2`
|
||||
|
||||
## Writeup
|
||||
by Cameron and [5225225](https://www.5snb.club)
|
||||
|
||||
Many optimized implementations of AES utilize lookup tables to combine all steps of each round of the algorithm (SubBytes, ShiftRows, MixColumns, AddKey) into a single operation. For some X (the plaintext or the result from the previous round) and some K (the round key), they are split bytewise and the XOR product of each respective byte pair is used as the index into a lookup table. During the first round of AES, X is the plaintext of the message, and K is the original message key. Accordingly, given some known plaintext, leaking the index into the lookup table for a particular character leaks the corresponding key byte. There are four lookup tables which are used in each iteration of AES (besides the last round) and which is used is determined by the index of the byte MOD 4. We utilized [this paper](http://www.jbonneau.com/doc/BM06-CHES-aes_cache_timing.pdf) as a reference for both our understanding of AES and the attack we will detail below.
|
||||
|
||||
Many CPUs cache RAM accesses so as to speed up subsequent accesses to the same address. This is done because accessing RAM is quite slow, and accessing cache is quite fast. This behavior would imply that on systems which implement such caching methods, there is a correlation between the amount of time it takes to encrypt a particular plaintext and the occurrences of repeated values of a plaintext byte XORd with a key byte. Accordingly, for every i, j, p<sub>i</sub> ⊕ p<sub>j</sub> in a family (with i, j being byte indexes, p being the plaintext, and families corresponding to which lookup table is being used), we calculate the average time to encrypt such a message over all messages. We then determine if for any pair of characters p<sub>i</sub>, p<sub>j</sub> there is a statistically significant shorter encryption time compared to the average. If so, we can conclude that <sub>i</sub> ⊕ k<sub>i</sub> = p<sub>j</sub> ⊕ k<sub>j</sub> => p<sub>i</sub> ⊕ p<sub>j</sub> = k<sub>i</sub> ⊕ k<sub>j</sub>. From this information, we gain a set of redundant system of equations relating different key bytes at different indexes with each other. It is important to note that in order for this attack to work, we must know at least one key byte in each family in order to actually solve each system of equations. Additionally, due to how cache works, this attack only leaks the most significant q bits (q being related to the number of items in a cache line). Once the set of possible partial keys (accounting for the ambiguity in the least significant bits of each derived byte) has been obtained by the above method, an attacker may brute force the remaining unknown key bytes.
|
||||
|
||||
In the case of Leaky Crypto, a set of 100,000 plaintexts and corresponding encryption times is provided along with the first six bytes of the encryption key. We ran an analyzer program[^1] against these plaintexts to obtain the probable correlation between different indexes in the key with respect to the XOR product of those bytes with plaintext bytes. Per the above, the plaintexts and timing data provided enough information to derive the systems of equations which may be used to solve for key bytes, and the first 6 bytes of the key provided enough information to actually solve said systems of equations. Given the ambiguity of the low bits of each derived key byte, we obtained 2<sup>14</sup> partial keys with three unknown bytes each. Thus, we reduced the problem of guessing 2<sup>128</sup> bits to guessing only 2<sup>38</sup> bits. We fed our derived partial keys into [Hulk](https://github.com/pgarba/Hulk) to brute force the remaining bytes for each candidate partial key. After 30 minutes had passed, we successfully brute forced the key.
|
||||
In the case of Leaky Crypto, a set of 100,000 plaintexts and corresponding encryption times is provided along with the first six bytes of the encryption key. We ran an analyzer program[^1] against these plaintexts to obtain the probable correlation between different indexes in the key with respect to the XOR product of those bytes with plaintext bytes. Per the above, the plaintexts and timing data provided enough information to derive the systems of equations which may be used to solve for key bytes, and the first 6 bytes of the key provided enough information to actually solve said systems of equations. Given the ambiguity of the low bits of each derived key byte, we obtained 2<sup>14</sup> partial keys with three unknown bytes each. Thus, we reduced the problem of guessing 2<sup>128</sup> bits to guessing only 2<sup>38</sup> bits.
|
||||
|
||||
[^1]: ```python
|
||||
from itertools import combinations
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
Since we only knew the most significant bits of most of the bytes in the key, we needed to use a candidate generator script[^2] in order to generate trial patterns, with the fully unknown bytes replaced with `??`. This was because we were using [Hulk](https://github.com/pgarba/Hulk) to brute force the keys, which did not support brute forcing the least significant bits of bytes, only fully unknown bytes.
|
||||
|
||||
def find_outliers(corpus, num_samps, i, j):
|
||||
idxs = corpus[i][j].argsort()[:num_samps]
|
||||
return idxs
|
||||
|
||||
def guess_bytes(corpus, known_keybytes, num_samps, avg):
|
||||
candidates = []
|
||||
for base in range(4):
|
||||
family = [base, base + 4, base + 8, base + 12]
|
||||
for combo in combinations(family, 2):
|
||||
i,j = combo
|
||||
guesses = find_outliers(corpus, num_samps, i, j)
|
||||
guesses2 = []
|
||||
for guess in guesses:
|
||||
cnt = corpus[i][j][guess]
|
||||
if cnt-avg < -10:
|
||||
guesses2.append((i, j, guess, cnt-avg))
|
||||
print(i, j, guess, cnt - avg)
|
||||
candidates.append(tuple(guesses2))
|
||||
print(candidates)
|
||||
|
||||
if __name__ == '__main__':
|
||||
known_keybytes = bytes.fromhex("64c7072487f2")
|
||||
secret_data = "c1a5fe7beb2c70bfab98926627dcff8b9671edc52441f89fa47797aa023f15f67907ee837b93cd9b194922ebb7c3ca3bd1cbfbc888efe147e80554047d82872fcee564c1bfd2e0a809568acb5cc08f4836a5f91f43b576a4ee1c6f097c15e1cd4056917fc51c1e5d8157409b11f1600d"
|
||||
|
||||
data = set()
|
||||
with open("test.txt", "r") as fp:
|
||||
for line in fp:
|
||||
pt, timing = line.strip().split(',')
|
||||
pt = bytes.fromhex(pt)
|
||||
timing = int(timing)
|
||||
data.add((pt, timing))
|
||||
|
||||
tavg = sum((d[1] for d in data)) / len(data)
|
||||
print("tavg: %d" % tavg)
|
||||
|
||||
known_tly = np.zeros((16, 16, 256))
|
||||
|
||||
for base in range(4):
|
||||
print("Building corpus for family %d" % base)
|
||||
family = [base, base + 4, base + 8, base + 12]
|
||||
for combo in combinations(family, 2):
|
||||
times = np.zeros(256)
|
||||
counts = np.zeros(256)
|
||||
i,j = combo
|
||||
print("Working on %d, %d" % (i, j))
|
||||
for d in data:
|
||||
n = d[0][i] ^ d[0][j]
|
||||
c = d[1]
|
||||
times[n] += c
|
||||
counts[n] += 1
|
||||
for c in range(256):
|
||||
cnorm = times[c] / counts[c]
|
||||
known_tly[i][j][c] = cnorm
|
||||
known_tly[j][i][c] = cnorm
|
||||
|
||||
guess_bytes(known_tly, known_keybytes, 4, tavg)
|
||||
```
|
||||
Since the given section of a flag is longer than 16 bytes, which is the size of an AES block, and the satellite is using Electronic Code Book mode, which means that all blocks are encrypted/decrypted separately, we could give hulk the first 16 bytes of the encrypted message as the ciphertext, the first 16 bytes of the flag as the expected plaintext, and then the key is all of the lines output from the `gen.py` script, which is the candidate generator script.
|
||||
|
||||
```bash
|
||||
python gen.py |
|
||||
xargs -I{} ./hulk d c1a5fe7beb2c70bfab98926627dcff8b 666c61677b726f6d656f383630353172 {} |
|
||||
tee output.log
|
||||
```
|
||||
|
||||
After 30 minutes had passed, we successfully brute forced the key, which could then be used to decrypt the rest of the flag.
|
||||
|
||||
[^1]: ```{.python include=attack.py}
|
||||
```
|
||||
|
||||
[^2]: ```{.python include=gen.py}
|
||||
```
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
guesses = {
|
||||
7: [52, 54, 53, 43],
|
||||
8: [149, 151, 150, 148],
|
||||
9: [173, 174, 175, 172],
|
||||
10: [83, 80, 81, 82],
|
||||
13: [186, 184, 185, 187],
|
||||
14: [2, 1, 0, 3],
|
||||
15: [151, 149, 150, 148]
|
||||
}
|
||||
|
||||
known_keybytes = bytes.fromhex("64c7072487f2")
|
||||
|
||||
candidates = [list(known_keybytes)]
|
||||
for i in range(len(known_keybytes), 16):
|
||||
new_candidates = []
|
||||
if i in guesses.keys():
|
||||
for guess in guesses[i]:
|
||||
for candidate in candidates:
|
||||
new_candidates.append([c for c in candidate] + [guess])
|
||||
else:
|
||||
for candidate in candidates:
|
||||
new_candidates.append([c for c in candidate] + [-1])
|
||||
candidates = new_candidates
|
||||
|
||||
print("Generated %d candidates" % len(candidates))
|
||||
for candidate in candidates:
|
||||
rep = ''.join([hex(c)[2:].zfill(2) if c != -1 else '??' for c in candidate])
|
||||
print(rep)
|
Loading…
Reference in New Issue