AES in ECB mode is deterministic and leaks information when the same key encrypts appended data, especially when an attacker can influence part of the plaintext.
The byte-at-a-time attack is a classic chosen-plaintext attack that recovers secret data (usually appended after attacker-controlled input) one byte at a time by aligning blocks and brute-forcing single bytes.
Core Requirements
- AES-128-ECB encryption oracle (you provide plaintext, get ciphertext)
- The oracle appends:
secret_key ^ (attacker_input || secret_data) - Block size is known (usually 16 bytes for AES)
High-Level Steps
- Determine block size (by watching when ciphertext length increases by 16 bytes)
- Find how many bytes of padding are needed so the next unknown byte falls at the end of a block
- Encrypt once to get the target ciphertext block
- Brute-force the last byte (printable range 32–127) until the block matches
- Append the discovered byte and repeat sliding the window one byte forward each time
Code Snippet: Alignment
# How many filler bytes do we need so the next unknown byte is at position 15 mod 16?
block_size = 16
known_bytes = b"already_recovered_prefix" # grows over time
filler_len = block_size - 1 - (len(known_bytes) % block_size)
filler = b"A" * filler_len
payload = filler + known_bytes
# -> send payload to oracle -> get ct
# target_block_offset = (len(payload) // block_size) * block_size
# target_ct_block = ct[target_block_offset : target_block_offset + block_size]
Code Snippet: Brute-force Loop
for guess in range(32, 128): # printable ASCII
trial = filler + known_bytes + bytes([guess])
trial_ct = encrypt_oracle(trial.hex()) # or however your oracle works
trial_block = trial_ct[target_block_offset : target_block_offset + block_size]
if trial_block == target_ct_block:
known_bytes += bytes([guess])
break
Handling Block Transitions
The beauty is that you don’t need special logic for new blocks.
As len(known_bytes) grows, filler_len automatically decreases, when it reaches 0, the next iteration will start aligning the next block.
Common Variations
- Only brute-force printable chars (32–127) if you know it's text/flag-like
- Handle PKCS#7 padding, sometimes the last 1–15 bytes are padding (you'll know if no more printable bytes match)
- Use caching: store previously seen ciphertexts for the same prefix to avoid redundant oracle calls
Why This Works
Because ECB encrypts each 16-byte block independently:
Input: AAAAAAAAAAAAAAAAX secretbyte...
Block: [ controlled + 1 unknown ] [ rest ]
When you guess the correct X, the entire block ciphertext matches,revealing one byte at a cost of ~256 queries.
Takeaway
Never use ECB when the same key encrypts attacker-influenced data appended with secret data. An attacker with enough time on his hands will be able to retrieve it.
Even though AES is strong, ECB turns it into a glorified hash table, perfect for pattern leakage and byte-by-byte recovery attacks like this one.
Tools used: Python + requests library (for oracle interaction)