AES ECB (Electronic CodeBook) mode is vulnerable to guess plaintext/ciphertext without knowing the key by using padding.
How It Works
In ECB mode, plaintext is separated into each block with fixed size (e.g. 16, 32, etc.) and encrypt individually, then each block will be concatenated at the end. Below is the flow.
# 1. Input plaintext ('1'*32) to encrypt11111111111111111111111111111111# 2. Separate into each block with 16-bytes size11111111111111111111111111111111# 3. Encrypt each blockENC(1111111111111111)ENC(1111111111111111)# 4. Concatenate each encrypted blockENC(1111111111111111)+ENC(1111111111111111)# 5. Convert to hex at the end for the outputHEX(ENC(1111111111111111)+ENC(1111111111111111))
If we input a plaintext which cannot be separated the same size e.g. 31 characters string (1-byte is missing) as below, the plaintext needs to be padded for adjusting the byte size before separating.
# 1. Input plaintext ('1'*31) to encrypt <- This is a half-assed!1111111111111111111111111111111# 2. Need to pad it for allowing to separate each block with the same size (31 bytes -> 32 bytes)1111111111111111111111111111111\x01# 3. Separate it into each block with 16-bytes size1111111111111111111111111111111\x01# 4. Encrypt each blockENC(1111111111111111)ENC(111111111111111\x01)# 5. Concatenate each encrypted textENC(1111111111111111)+ENC(111111111111111\x01)# 6. Convert to hex at the endHEX(ENC(1111111111111111)+ENC(111111111111111\x01))
As above, we can read each encrypted block (ENC(1111111111111111) and ENC(111111111111111\x01)) from the encrypted text because these are just concatenated.
Below is the example for reading each block.
Using this separating mechanism, we can manipulate plaintext and retrieve the FLAG without knowing the secret key.
Exploitation (Example Challenge)
The following Python script encrypts 'arbitrary plaintext' + FLAG with AES-ECB mode. Assume that we don't know the secret key and FLAG text.
Our challenge is to find the FLAG text.
If we input 31-bytes string ('1' * 31), the string will be padded and separated into each block with 16-bytes size. As a result, the middle block will be '111111111111111F'.
# 1. Our plaintext ('1'*31) + 'FLAG{unknown}'1111111111111111111111111111111FLAG{unknown}# 2. Separate it into each block with 16-bytes. The last string is padded to 16-bytes.1111111111111111 111111111111111F LAG{unknown}\x04\x04\x04\x04
At this point, try to input the string ('1' * 31 + 'F') as plaintext. What will be the each block?
As above, the middle block ('111111111111111F') will be the same as the previous one.
It means that our first input ('1' * 31) and the second input ('1' * 31 + 'F') will lead the script to generate the same middle block.
Next we compare the input ('1' * 30) and the input ('1' * 30 + 'FL'). The middle block will be such the following after separating.
Representing the above in a Python script would look like this:
# Extract the middle block by '[16:32]'.print(encrypt(b'1'*31)[16:32] ==encrypt(b'1'*31+b'F')[16:32])# Trueprint(encrypt(b'1'*30)[16:32] ==encrypt(b'1'*30+b'FL')[16:32]# True
2. Brute Force
Using the above mechanism, we can find the FLAG by bruteforcing characters while decreasing the number of '1' ('1'*31, '1'*30, '1'*29, …).
Comparing the first input ('1' * N) and the second input ('1' * N + 'some characters'), we will be able to find the FLAG.
Below is the Python script for doing that.
defbruteforce(): flag ='' total =32-1 chars ='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-~!?#%&@{}'whileTrue: payload ='1'* (total -len(flag)) ciphertext_1 =encrypt(payload.encode())for c in chars: ciphertext_2 =encrypt((payload + flag + c).encode())# Comapare the middle blocks ([32:64]) of each encrypted textif ciphertext_2[32:64]== ciphertext_1[32:64]: flag += cprint(flag)breakbruteforce()