Overview
This article summarizes classic problems from Cryptopals (mainly sets 2–3) and related AES patterns. AES is a block cipher; the mode determines security properties. We’ll focus on three modes:
- ECB (Electronic Codebook)
- CBC (Cipher Block Chaining)
- CTR (Counter mode)
ECB
Padding. Pad to a multiple of 16 bytes (PKCS#7). For example:
len(message) → multiple of 16 via PKCS#7
pad with N bytes of value N (N between 1 and 16) so that after padding,
the total length is a multiple of 16.
Detecting ECB
If an oracle encrypts prefix || message || suffix under AES-ECB, identical plaintext blocks produce identical ciphertext blocks. Send a repeated block to spot duplication:
# Pseudocode
msg = b"a" * 100
ct = AES_ECB_Encrypt(pad(prefix + msg + suffix))
# If you see multiple identical 16-byte chunks repeating in ct, it's likely ECB.
Byte-at-a-time (suffix recovery)
Assume the prefix is empty for simplicity. To recover the first unknown byte of suffix, send 15 bytes of ‘a’ so the first block is ECB("a"*15 + suffix[0]). Brute-force the last byte until the ciphertext block matches; iterate to recover subsequent bytes.
- Question: How to adapt when there is a fixed, unknown prefix?
- Question: Why this method cannot recover the prefix?
Cut-and-paste (role=admin)
Because ECB encrypts blocks independently, you can splice ciphertext blocks to craft a desired plaintext:
Backend: AES_ECB(pad("email=<...>&uid=10&role=user"))
Goal: AES_ECB(pad("email=<...>&uid=10&role=admin"))
# Craft inputs so the block containing "admin" aligns and can be transplanted.
CBC
Similar to the ECB cut-and-paste attack, the bit-flipping attack allows us to manipulate the ciphertext so that, after decryption, we obtain the desired message.
The attack exploits the XOR process that occurs during CBC decryption. Suppose we have a CBC encryption oracle of the form:
ciphertext = AES-CBC(pad(prefix + message + suffix))
We can freely choose the message input, but certain characters such as ; and = are sanitized and cannot appear directly. Our goal is to create a ciphertext that, when decrypted, contains the substring:
;admin=true;
To simplify, let us assume that the length of the prefix is a multiple of 16. In CBC mode, the encryption of a given block is not affected by the subsequent blocks of the input message. We encrypt the following message:
original_message + "*admin=true*****"
and then adjusts the last second block (using XOR) so that "*admin=true*" becomes ";admin=true;".
CBC padding oracle
Imagine there is a padding oracle that works as follows:
encrypted_message -> decrypt -> check PKCS#7 padding (return true/false)
This oracle tells you whether the encrypted message corresponds to a correctly padded plaintext. It turns out that with such a padding oracle, we can fully decrypt the message. The idea is simple: we take an arbitrary 16-byte encrypted block and craft an IV like "\x00" * 15 + ?. If this ciphertext passes the oracle's check, then it is very likely that
brute force IV[-1] so that it passed the oracle check, which means:
IV[-1] ^ xor(previous encrypted block, raw message) = \x01 ->
xor(previous encrypted block, raw message)[-1] = \x01 ^ IV[-1]
This IV[-1] will be used in the next paragraph.
Next, we set the IV to "\x00" * 14 + IV[-2] + \x01 ^ \x02 ^ IV[-1] , this guarantees that the last byte after xor with IV is \x02. Then brute force process until the oracle returns true. This tells us that
IV[-2] ^ xor(previous encrypted block, raw message)[-2] = \x02 ->
xor(previous encrypted block, raw message)[-2] = \x02 ^ IV[-2].
Continue this idea, we shall be able to recover xor(previous encrypted block, raw_message_block) , since previous encrypted block is known, we can recover raw_message_block .
CTR
CTR mode is a stream cipher; even blocks with the same plaintext content will produce different ciphertext. CTR generates the keystream by encrypting the nonce together with a counter.
keystream_i = AES.encrypt(nonce (8 bytes) || counter (8 bytes, little endian))
The nonce is used to prevent replay attacks and to ensure that the keystream is not reused. For a fixed nonce, the keystream is composed as follows:
(first 16 bytes of keystream) keystream_0 = AES.encrypt(nonce || "\x00"*8)
(next 16 bytes of keystream) keystream_1 = AES.encrypt(nonce || "\x01" + "\x00"*7)
(next 16 bytes of keystream) keystream_2 = AES.encrypt(nonce || "\x02" + "\x00"*7)
Fixed-nonce vulnerability
When CTR mode reuses the same nonce to encrypt different messages, the keystream becomes vulnerable. The attack reduces to a single-byte XOR problem, where letter frequency analysis can be used to recover the keystream.