cryptofreeeasy

Broken Decryptor

hackthebox

Task: AES-CTR encryption with biased OTP (no 0x00 bytes) and broken decrypt oracle. Solution: Statistical byte elimination attack - collect ~2000 samples to identify the one missing byte value at each position, then XOR with keystream recovered from encrypting zeros.

$ ls tags/ techniques/
known_plaintext_attackstatistical_byte_eliminationkeystream_reuse_attack

$ cat /etc/rate-limit

Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.

Broken Decryptor — HackTheBox

Description

"The decrypt function is broken and I lost my flag. Can you help me fix it?"

A Python server implements AES-CTR encryption with a twist. Players connect to a TCP service with 3 options: get encrypted flag, encrypt a message, or decrypt a message. The decrypt function is intentionally broken (calls .encode() on bytes, which crashes in Python 3).

Analysis

Key Source Code

key = os.urandom(0x10).replace(b'\x00', b'\xff') iv = os.urandom(0x10).replace(b'\x00', b'\xff') def encrypt(data): ctr = Counter.new(128, initial_value=int(iv.hex(), 16)) crypto = AES.new(key, AES.MODE_CTR, counter=ctr) if type(data) != bytes: data = data.encode() otp = os.urandom(len(data)).replace(b'\x00', b'\xff') return xor(crypto.encrypt(data), otp) def decrypt(data): ctr = Counter.new(128, initial_value=int(iv.hex(), 16)) crypto = AES.new(key, AES.MODE_CTR, counter=ctr) return crypto.decrypt(data.encode()) # BUG: .encode() on bytes crashes

Vulnerabilities Identified

  1. Broken decrypt function: The decrypt() function calls .encode() on a bytes object, which crashes in Python 3. This is the "broken" part mentioned in the challenge title.

  2. Biased OTP generation: The OTP is generated as os.urandom(len(data)).replace(b'\x00', b'\xff'). This means OTP bytes range over 1-255, never 0x00.

  3. Fixed AES-CTR keystream: The key and IV are fixed for the session, so the AES-CTR keystream is identical for every encryption call.

Attack: Statistical Byte Elimination

Since encrypt(data) = data ⊕ keystream ⊕ OTP and OTP byte is never 0x00:

  • The output byte at position j is data[j] ⊕ KS[j] ⊕ OTP[j]
  • Since OTP[j] takes all values in {1..255} but never 0, the value data[j] ⊕ KS[j] ⊕ 0 = data[j] ⊕ KS[j] never appears in the output
  • By collecting ~2000+ samples, we can identify the one missing byte value at each position

...

$ grep --similar

Similar writeups