The Adventure of Attacking AES
April 16, 2023 • ☕️☕️☕️☕️ 18 min read
In this article, I want to share my personal learning journey and experience on how I practiced to do cryptanalysis on reduced round AES step by step from the easiest one which is 1-round AES until 3-round AES.
The attack that we will described here just mainly focus on the differential cryptanalysis of AES S-Box. According to the AES proposal [1], it is claimed that AES is resistant against differential cryptanalysis above 4-round. So, how about round below it? Lets find out!
Preliminaries
Before we dive into the attack, it’s good idea to refresh our reference on short preliminaries about AES operations and basic of the attack that we gonna use.
AES
Advanced Encryption Standard (AES) is the algorithm that was selected by NIST that originally named Rijndael. AES use Substitution-Permutation Network (SPN) as its main design.
AES is 128-bit block cipher which its state represented as byte matrix of size 4x4. It consist of 4 main operations in single round which are SubBytes
, ShiftRows
, MixColumns
, and AddRoundKey
.
The number of rounds used in AES depends on the key size, where 128-bit use 10 rounds, 192-bit use 12 rounds, and 256-bit use 14 rounds. In the first round, an additional AddRoundKey
is used, and in the last round, MixColumns
operation is omitted.
This is how AES encryption works in nutshell:
def AES(plaintext, master_key, n_rounds=10):
round_key = key_schedule(master_key)
state = add_round_key(plaintext, round_key[0])
for i in range(1, n_rounds):
state = sub_bytes(state)
state = shift_rows(state)
state = mix_columns(state)
state = add_round_key(state, round_key[i])
# last round without MixColumns
state = sub_bytes(state)
state = shift_rows(state)
state = add_round_key(state, round_key[n_rounds])
return state
SubBytes
SubBytes
will apply substitution mapping on each byte of the state against S-Box. This operation is the only operation in AES that is not linear.
ShiftRows
ShiftRows
is the operation that will shift byte position each row to the left depending on the row. Second row will be shifted 1 to the left, third row shifted 2 to the left, and fourth row shifted 3 to the left (first row stay the same).
MixColumns
The third operation which is MixColumns
will transform all bytes in the same column by multiply them with the constant 4x4 Matrix over the field .
NOTE: we can treat this operation as black box function that transform 4-byte input into another 4-byte output.
AddRoundKey
The last operation is AddRoundKey
which simply XOR the state with the RoundKey
.
RoundKey
is the subkey that expanded from MasterKey
using the Key Schedule function, so it will generate different RoundKey
on each round of AES.
We can ignore how the Key Schedule function works since this is irrelevant with our attack. The only thing we must remember here is with any known RoundKey
, we can recover its MasterKey
since Key Schedule function is invertible.
Differential Cryptanalysis
Differential Cryptanalysis is one of the most powerful method to attack block cipher and together with the Linear Cryptanalysis become two de facto standard of cryptanalysis against block cipher.
Differential cryptanalysis usually uses bitwise XOR to define the “difference”. XOR operation is very useful given the fact that it can “bypass” or skip linear operation on block cipher.
In the context of AES, assume we have the following equation:
where , , and denote AddRoundKey
, MixColumns
, and ShifRows
operation respectively.
Then, lets assume differential of two plaintext and ciphertext is XOR between these pair that denoted by and respectively, then the equation become:
Since is just XOR state with the RoundKey
and both of and XORed with the same RoundKey
, we can simplify the equation by removing at all.
And that’s it, we basically already bypass all linear operation even without the knowledge of RoundKey
.
Imagine AES without
SubBytes
operation, then if you knowc1
,c2
, andp1
, it is become trivial to recoverp2
, just like what happen in simple XOR cipher.
This way, we only need to focus on one operation that is non-linear which is SubBytes
.
Consider for every possible byte values. Then, let and where denotes SubBytes
operation. We are trying to analyze how many times and hold.
For example, let and . Over all 256 possible bytes, there are two possible byte pair that satisfy the condition which are and .
So, the probability of (, ) being are 2/256.
And that is what Differential Cryptanalysis is all about, we find the probability of input differential () that will resulting to the specific output differential () through S-BOX.
Differential in AES S-box
In order to find differential property of S-Box, we must first find the distribution of the differences between pairs of input and pairs of output values. This can be achieved from S-Box Differential Distribution Table (DDT), which is the table that shows the number of occurences every possible input difference that results in coresponding output difference.
S-Box DDT can be quickly built with the following Python code:
SBOX = [...] # 1D list of AES S-Box
def generate_sbox_ddt():
table = [[]] * 256
for i in range(256):
for j in range(256):
diff_input = i ^ j
diff_output = SBOX[i] ^ SBOX[j]
if len(table[diff_input]) != 0:
table[diff_input][diff_output] += 1
else:
table[diff_input] = [0] * 256
table[diff_input][diff_output] = 1
return table
If we look closely on the S-Box DDT result, each row of table have kind of similar occurence distribution where it contains either 0 or 2 and one time occurence of 4 (except the first row where input equal to zero).
Below is the sample of the result showing 2nd and 65th row:
[0, 2, 0, 0, 2, 0, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 0, 2, 2, 0, 0, 2, 2, 2, 0, 0, 0, 2, 4, 0, 2, 2, 0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 2, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 0, 0, 0, 2, 2, 2, 2, 0, 2, 0, 0, 0, 2, 0, 0, 2, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 2, 0, 2, 2, 2, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 0, 0, 2, 0, 2, 0, 0, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 0, 2, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 0, 2, 0, 2, 2, 2, 2, 0, 0, 2, 0, 2, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 0, 2, 2, 2, 2, 2, 0, 2, 2, 0, 0, 0, 0, 2, 0, 0, 0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 0, 0, 2, 0, 0, 2, 0, 0, 2, 2, 2, 0, 0, 2, 0, 0, 0, 2, 2, 2, 0, 2, 2, 0, 0, 0, 2]
[0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 2, 2, 2, 0, 0, 2, 2, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 0, 0, 2, 0, 0, 0, 2, 0, 2, 0, 2, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 4, 2, 0, 0, 0, 2, 0, 2, 2, 0, 2, 2, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 2, 2, 0, 0, 2, 0, 2, 0, 2, 0, 0, 0, 2, 0, 2, 2, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 0, 2, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 0, 2, 2, 0, 2, 0, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 0, 2, 0, 0, 0, 2, 0, 0, 2, 2, 2, 0, 0, 0, 2]
If we try to count those, we will find that on each row (except 1st row), 0 appears 129 times, 2 appears 126 times, and 4 appears 1 time.
Lets consider as input and output difference pairs of single AES S-box and . Then we have following observation based from the S-Box DDT:
- For 129/256 of possible pairs , there is no such pair that and
- For 126/256 of possible pairs , there exist two pairs such that and
- For 1/256 of possible pairs , there exist four pairs such that and
From the above observation, can be implied that if we know a pair of then we can quickly recover which is the actual input/output value of the S-Box [2].
1 Round AES
Let’s start breaking 1 round AES with the differential cryptanalysis!
Consider and are plaintext-ciphertext pairs of 1-round AES, where all these values are known to us and we want to find the MasterKey
(known-plaintext scenario).
Let differential of denoted by and differential of denoted by .
We then apply and to . Now we have one-on-one byte relation of and through S-Box.
Then, consider and where we try all possible values until they satisfy following conditions:
- .
According to the differential observation that explained before, it should exist two possible pairs that satisfy those conditions.
Alternatively, we can optimize our finding of possible value using S-Box DDT that we built previously. This way, we can just do table lookup of and to quickly find possible instead of using brute-force. But, we need to slightly modify our S-Box DDT to store the actual value of instead of the number of occurence.
Below is the Python implementation that generate S-Box DDT lookup:
SBOX = [...] # 1D list of AES S-Box
def generate_sbox_ddt():
table = [[]] * 256
for i in range(256):
for j in range(256):
diff_input = i ^ j
diff_output = SBOX[i] ^ SBOX[j]
if len(table[diff_input]) != 0:
table[diff_input][diff_output].update(set([i, j]))
else:
table[diff_input] = [set() for _ in range(256)]
table[diff_input][diff_output] = set([i, j])
return table
ddt = generate_sbox_ddt()
# Lookup the table
alpha = ...
beta = ...
possible_xy = ddt[alpha][beta]
Okay, so what’s next after we found the value of ?
Remember that before round 1, there is additional AddRoundKey
before the very first SubBytes
operation. So, the value of is the value of either or . Thus, we can find possible by simply XORing or with the respective byte of or .
Thats it! We can iterate this process for all byte position of . Note that there will always be 2 possible values for each byte position of (except the differential is zero), so we can do trial encryption/decryption with the obtained possible key to get the correct key with only trials.
NOTE: Because we already found
RoundKey
of round 0, we don’t need to reverse the key expansion since thisRoundKey
is already equal to the AESMasterKey
2 Round AES
Can we extend our previous attack to make it works against 2 round AES?
Consider and are plaintext-ciphertext pairs of 2-round AES and are input-output difference pairs of and respectively.
If we illustrate this scenario into 2-round AES, we can bypass first AddRoundKey
and ShiftRows
by swapping operation order with SubBytes
. Then, in the last round, we can bypass ShiftRows
, MixColumns
, and AddRoundKey
. So, the view become .
Unfortunately, there are already too many operation between SubBytes
that will significantly mess our differential distribution. Moreover, MixColumns
will also prevent us to work on one-on-one byte relation, since this operation works on 4-bytes at once. This is already too much and too complex. Thus, we need different strategy that much more simple.
The key point here is that we can only “bypass” SubBytes
with differential once at a time. But then, there are two SubBytes
in 2-round AES, how can we bypass the other one?
Half Last Round
To simplify our understanding of the attack, lets first assume that last round does not make use of MixColumns
(usually called half round). So, in the round 2, the operation flow is only .
Let be differential of plaintext where only the first column has active byte, which is byte with non-zero differential. Any other value should be zero differential or inactive byte. Since we need this spesific plaintext value, so this attack only applicable in chosen-plaintext scenario.
Then, send to the encryption oracle which will give us corresponding .
If we remember our previous differential observation, there are 129/256 probability that differential input/output is impossible. Instead of using possible pair of differential, we will use the opposite which is the impossible pair. We can use this property as distinguisher whether our state is in correct state or not.
So, the main idea here is to generate all impossible state at the start of round 2 just before second SubBytes
using . First, we want to bypass SubBytes
using S-Box DDT like our previous attack on 1-round. But here, instead of finding possible S-Box output differential, we want to find impossible S-Box output differential. This will result in 129 impossible byte for each denoted by . Then, for each we apply , and store this state to the list of impossible state.
If we look at the image above, our impossible state is spreaded to all byte position despite only using 4-bytes. That is the main reason why we put active byte only on the same column.
Then, we will guess one byte of for every possible values and inverse the pair of ciphertext through , , and independently. Finally, we create differential of inversed denoted by and compare with our generated impossible state. If exist in the impossible state, then we know that our guess key is incorrect and we add it to the list of impossible key.
The remaining key that is not discarded to the list of impossible key should be the correct possible key. We just need to repeat this process to the other byte position.
But, there is still one problem, that is if we only use one , it is not enough to discard most of the impossible key. From my experiment, single will only find about 110-140 impossible keys. So, we need more which means more chosen-plaintext pairs. The ideal number of required would be about ~10 to discard almost all of the impossible keys.
NOTE: using two doesn’t mean we will find ~250 impossible keys, since most keys found will be duplicate to each other.
After we discard almost all of the impossible keys, we still need to inverse the key from to which is the MasterKey
. How we perform inverse Key Schedule is outside this article scope and will not be explained here. If you want to learn how to do it, you can take a look on my source code or probably implement yourself :D
Finally, we can enumerate the remaining possible key using trial encryption/decryption like our previous attack. This should only take less than trials if we use ~10 .
Full Last Round
Now, as we successfully broke 2-round AES with half last round, how will we do it in the full last round?
If we re-illustrate the attack flow into full round scenario, we need 4-byte guess at once to bypass InvMixColumns
operation and this alone already significantly increase time complexity from to .
Fortunately, there is one property in AES to overcome this problem. Remember that both of MixColumns
and AddRoundKey
are linear operation and these operation hold the following equation:
Which means we can swap the order operation of MixColumns
with AddRoundKey
as well as AddRoundKey
with InvMixColumns
.
So, we can apply on our before as long as we also apply to the . But, we don’t really care about updating right now. We just need to apply later when we got all bytes of the possible keys.
Thats it! There is no other difference with how we attack half round.
3 Round AES
Good news! We can actually extend our previous attack to work against 3-round AES. We can think this as “2-round AES with additional round at the beginning”.
Let be differential of plaintext where only the first byte has active byte, the rest are inactive. If we illustrate this state and go through first round transformation, the state will end up exactly as the state in the first round of 2-round attack.
Okay, so the only problem now is how can we bypass this SubBytes
in the first round?
We can guess one byte of for every possible values denoted by and then apply 1 round transformation to the plaintext through , , , and independently (note that we apply of round 0, instead of round 1). After that, we create differential of transformed denoted by and finally we are in the same situation as the previous 2-round attack.
From here, the flow is relatively same as 2-round attack where we generate bunch of impossible state from through second round resulting the list of impossible state.
Then, we guess another one byte of denoted by , inverse pair of ciphertext in the third round through , , , independently and create differential from those pair denoted by . Compare with the impossible state, if exist in one of the impossible state, we add to the list of impossible key.
We can determine whether is the correct guess or not by looking at the number of that added to the impossible keys. If in any byte position, impossible keys contain all possible bytes, then is incorrect, otherwise is correct.
After impossible keys of all byte position in discarded, we enumerate the remaining key, apply and perform inverse key schedule to reverse to and check if the key is correct via trial encryption/decryption.
Although the chosen-plaintext required in this attack is the same with the previous attack (~10 ), the time complexity in this attack is at least times higher than the previous attack since we guess 2 bytes at a time. Moreover, for each guess of , we need to regenerate the impossible state again. But this attack is still quite fast and running this on my laptop only take less than 5 minutes on average.
Conclusion
That’s all, we successfully broke 1-round AES until 3-round AES with differential cryptanalysis. We start ourselves looking at the differential property of AES S-Box and then able to exploit those property to recover the key round by round.
Although there are many of more sophisticated and more advanced attacks on higher round, AES is still remain secure in full round.
Lastly, all python implementation of these attacks can be found here.
References
-
Daemen, J. & Rijmen, V. (1998). AES Proposal: Rijndael, NIST AES proposal.
-
Bouillaguet, C., Derbez, P., Dunkelman, O., Keller, N., Rijmen, V., & Fouque, P.-A. (2010). Low Data Complexity Attacks on AES. Cryptology ePrint Archive, Paper 2010/633. https://eprint.iacr.org/2010/633