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

  1. Determine block size (by watching when ciphertext length increases by 16 bytes)
  2. Find how many bytes of padding are needed so the next unknown byte falls at the end of a block
  3. Encrypt once to get the target ciphertext block
  4. Brute-force the last byte (printable range 32–127) until the block matches
  5. 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)