Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
174 lines (141 sloc)
5.17 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import sys | |
import binascii | |
import itertools | |
import collections | |
sbox = [ | |
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, | |
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, | |
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, | |
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, | |
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, | |
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, | |
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, | |
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, | |
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, | |
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, | |
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, | |
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, | |
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, | |
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, | |
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, | |
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 | |
] | |
# shift row permutation on indexes | |
shift_rows = [ | |
0, 13, 10, 7, | |
4, 1, 14, 11, | |
8, 5, 2, 15, | |
12, 9, 6, 3, | |
] | |
# indexes for every column | |
cols = [ | |
(0, 1, 2, 3), | |
(4, 5, 6, 7), | |
(8, 9, 10, 11), | |
(12, 13, 14, 15) | |
] | |
def inv_perm(perm): | |
inv = [0] * len(perm) | |
for i in range(len(perm)): | |
inv[perm[i]] = i | |
return inv | |
sboxI = inv_perm(sbox) | |
def mult(a, b): | |
assert 0x100 > a >= 0 | |
assert 0x100 > b >= 0 | |
r = 0 | |
p = 0b100011011 | |
while b: | |
if b & 1: | |
r ^= a | |
a <<= 1 | |
b >>= 1 | |
if a >> 8: | |
a ^= p | |
return r | |
def mix_column(v): | |
assert len(v) == 4 | |
r1 = mult(2, v[0]) ^ mult(3, v[1]) ^ v[2] ^ v[3] | |
r2 = v[0] ^ mult(2, v[1]) ^ mult(3, v[2]) ^ v[3] | |
r3 = v[0] ^ v[1] ^ mult(2, v[2]) ^ mult(3, v[3]) | |
r4 = mult(3, v[0]) ^ v[1] ^ v[2] ^ mult(2, v[3]) | |
return (r1, r2, r3, r4) | |
def mix_fault(d, row): | |
col = [0, 0, 0, 0] | |
col[row] = d | |
return mix_column(col) | |
Ds = [] | |
for row in range(4): | |
for d in range(1, 0x100): | |
Ds.append(mix_fault(d, row)) | |
def recover(ct_correct, ct_fault, I, K): | |
assert len(I) == 4 | |
assert len(ct_correct) == len(ct_fault) == 16 | |
def solve(xA, xB, D): | |
ks = [] | |
for k in range(0x100): | |
if D == sboxI[xA ^ k] ^ sboxI[xB ^ k]: | |
ks.append(k) | |
assert len(ks) in (0, 2, 4) | |
return ks | |
for D in Ds: | |
# calculate candiates for each byte | |
ks = [[], [], [], []] | |
for j, i in enumerate(I): | |
ks[j] = solve(ct_correct[i], ct_fault[i], D[j]) | |
if len(ks[j]) == 0: | |
break | |
# expand the cross product to get full 32-bit keys | |
for k in itertools.product(*ks): | |
K[k] += 1 | |
def diff(s1, s2): | |
idx = set([]) | |
for i in range(len(s1)): | |
if s1[i] != s2[i]: | |
idx.add(i) | |
return frozenset(idx) | |
if __name__ == '__main__': | |
# sample file | |
with open(sys.argv[1], 'r') as f: | |
samples = map(str.strip, f.readlines()) | |
samples = [binascii.unhexlify(s) for s in samples] | |
# correct ciphertext | |
correct = binascii.unhexlify(sys.argv[2]) | |
# optional threshold | |
try: | |
threshold = int(sys.argv[3]) | |
except IndexError: | |
threshold = None | |
# group ciphertext by faulty indexes | |
Is = [tuple([shift_rows[v] for v in col]) for col in cols] | |
groups = { I: [] for I in Is} | |
for sample in samples: | |
df = diff(sample, correct) | |
for I in Is: | |
if all([i in df for i in I]): | |
groups[I].append(sample) | |
# extract from every group of faulty indexes | |
KEY = [None] * 16 | |
for I in Is: | |
print('I : %14s, ciphertexts: %d' % (I, len(groups[I]))) | |
# find the most seen 32-bit key candidates | |
K = collections.Counter() | |
for i, sample in enumerate(groups[I]): | |
recover(correct, sample, I, K) | |
# print status | |
show = ''.join(['%02x' % v if v else '??' for v in KEY]) | |
print('Key: %s, Sample %d / %d, Cand: %d, Top: %s' % (show, i, len(groups[I]), len(K), K.most_common(2))) | |
# check optional threshold | |
try: | |
_, cnt = K.most_common(1)[0] | |
if cnt >= threshold: | |
break | |
except: | |
pass | |
# fill in the full round key with the recovered 32-bits | |
key, _ = K.most_common(1)[0] | |
for j, i in enumerate(I): | |
assert KEY[i] is None | |
KEY[i] = key[j] | |
print('KEY:', bytes(KEY).hex()) |