Don’t use ECB mode for encryption · Zola’s Weblog
At By
Zola Gonano
I lately began doing CTF challenges. A couple of days in the past, I used to be engaged on a problem from 247CTF.com. I discovered a problem that, in my view, exhibits why utilizing ECB(Electronic Codebook) mode for encrypting with block ciphers like AES or Twofish isn’t a good suggestion. So, I made a decision to put in writing a collection of weblog posts the place I resolve these challenges and clarify methods to stop these sorts of assaults.
The problem was fairly easy. It was an internet site with two components: /encrypt
and /get_flag
. Each components wanted a hex-encoded message referred to as person
.
Understanding the Supply Code
This problem supplied the supply code for us, making it fairly simple to reverse engineer and perceive the way it works:
from Crypto.Cipher import AES
from flask import Flask, request
from secret import flag, aes_key, secret_key
app = Flask(__name__)
app.config['SECRET_KEY'] = secret_key
app.config['DEBUG'] = False
flag_user = 'impossible_flag_user'
class AESCipher():
def __init__(self):
self.key = aes_key
self.cipher = AES.new(self.key, AES.MODE_ECB)
self.pad = lambda s: s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size)
self.unpad = lambda s: s[:-ord(s[len(s) - 1:])]
def encrypt(self, plaintext):
return self.cipher.encrypt(self.pad(plaintext)).encode('hex')
def decrypt(self, encrypted):
return self.unpad(self.cipher.decrypt(encrypted.decode('hex')))
@app.route("/")
def major():
return "
%s
" % open(__file__).learn()
@app.route("/encrypt")
def encrypt():
strive:
person = request.args.get('person').decode('hex')
if person == flag_user:
return 'No dishonest!'
return AESCipher().encrypt(person)
besides:
return 'One thing went flawed!'
@app.route("/get_flag")
def get_flag():
strive:
if AESCipher().decrypt(request.args.get('person')) == flag_user:
return flag
else:
return 'Invalid person!'
besides:
return 'One thing went flawed!'
if __name__ == "__main__":
app.run()
Simply by trying on the code, it’s apparent that we have to encrypt the impossible_flag_user
utilizing the AESCipher
class outlined within the code. The category employs an easy algorithm for padding and makes use of AES with ECB mode for encryption. The key secret’s imported from one other Python file, which we don’t have entry to. This implies we will’t merely rewrite the AESCipher
class and encrypt no matter we would like.
However, the /encrypt
route takes a hex-encoded payload named person
and decodes it. If the decoded worth is the same as impossible_flag_user
, it returns a ‘No dishonest!’ message. Nevertheless, to acquire the flag, we have to present the /get_flag
route with a hex-encoded payload named person
that, when decrypted, equals impossible_flag_user
.
@app.route("/encrypt")
def encrypt():
strive:
person = request.args.get('person').decode('hex')
if person == flag_user:
return 'No dishonest!'
return AESCipher().encrypt(person)
besides:
return 'One thing went flawed!'
So, what we will do is assault the implementation of the encryption scheme, which is the AESCipher
class. The 2 major points that come to thoughts when it are the self-rolled padding algorithm and the usage of ECB mode.
However what’s ECB mode and the way can it assist us bypass that restriction? Effectively, ECB mode is the best approach of encrypting blocks in block cipher algorithms. It really works by breaking down the plaintext knowledge into blocks of a set dimension and encrypting every block with the important thing. This course of is repeated for every chunk till it reaches the final block. The ultimate block is then padded to match the block dimension of the block cipher, and all of the blocks are organized in collection to type the ciphertext:
However what’s the issue? ECB mode lacks diffusion, which means it doesn’t obscure the correlation between the plaintext and the ciphertext. This weak point is what we are going to leverage to our benefit when encrypting the impossible_flag_user
with it.
Performing the Assault
The very first thing that got here to my thoughts was that I may encrypt the impossible_flag_user
partially to acquire some encrypted segments. To attain this, I changed the person
within the plaintext with 0000
to keep up the identical size. Listed here are the outcomes:
939454b054b7379b0709a270b894025c1c3b822d1217b7af1516eccddb9349fc
Subsequent, I encrypted solely the person
and obtained the next outcome:
707ece4f0913868ec5df07d131b0822d
Now, all I needed to do was substitute one block dimension size (16 bytes on this case) from the primary encrypted plaintext with the corresponding portion from the second encrypted plaintext:
939454b054b7379b0709a270b894025c707ece4f0913868ec5df07d131b0822d
Now, by sending this modified ciphertext to the /get_flag
route, we receive the flag:
247CTF{ddd01e396dc1965c3fcf943f3968aa39}
The rationale this occurred is that the person
was our final chunk, and since there was no random initializing vector, irrespective of what number of occasions we encrypt that final chunk, we’d get the identical outcome. Basically, we encrypted the preliminary chunks after which appended the final chunk to bypass the restriction and acquire the flag.
This assault may have been simply prevented by utilizing a cipher mode that gives diffusion and authentication, comparable to GCM_SIV. This mode eliminates the necessity for padding, and the ciphertext will be authenticated later.
This weblog is out there on my GitHub, and for those who discover the content material fascinating, you may give it a star or take into account making a donation here.