← Back to Writeups
CUSTOMMediumCrypto

Noncense

XESXOR9/2/20263 min read
#crypto#custom#medium#ctr_nonce_reuse_attack#keystream_recovery#known_plaintext_attack

Noncense

Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: ctr_nonce_reuse_attack, keystream_recovery, known_plaintext_attack

Summary

Walkthrough of the CUSTOM Crypto Noncense. Task: AES-CTR encryption with /dev/zero as random source producing zero nonce. Solution: Known plaintext attack using flag prefix to recover keystream, then XOR to decrypt.

Description

AES-CTR encryption task where get_random_bytes reads from /dev/zero instead of /dev/urandom.

Analysis

Vulnerability: The random number generator reads from /dev/zero, producing all-zero bytes. This means:

  • Nonce = 0x00000000... (all zeros)
  • Every encryption uses the same nonce
  • Same keystream is generated for every block

AES-CTR Properties:

  • CTR mode: ciphertext = plaintext XOR keystream
  • Keystream depends only on key and nonce/counter
  • With fixed nonce, keystream is deterministic ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Attack: Known plaintext attack

  • We know the flag starts with "spbctf{" (7 bytes)
  • We can guess more: "spbctf{how_" (11 bytes) based on task theme
  • XOR known plaintext with ciphertext to recover keystream
  • Use keystream to decrypt remaining bytes

Solution

  1. Identify that nonce is all zeros (from /dev/zero)
  2. Use known flag prefix to recover keystream bytes
  3. Apply PKCS7 padding analysis to recover more keystream
  4. XOR ciphertext with recovered keystream
python
#!/usr/bin/env python3
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def xor_bytes(a, b):
    """XOR two byte strings"""
    return bytes(x ^ y for x, y in zip(a, b))

# Ciphertext (hex)
ciphertext = bytes.fromhex("...")

# Known plaintext prefix (flag format)
known_prefix = b"spbctf{how_"  # 11 bytes known

# Recover keystream from known plaintext
keystream_partial = xor_bytes(ciphertext[:len(known_prefix)], known_prefix)
print(f"Recovered keystream (first {len(known_prefix)} bytes): {keystream_partial.hex()}")

# For CTR mode with zero nonce, keystream repeats every 16 bytes (AES block)
# But counter increments, so we need full keystream
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Method 1: If we have multiple ciphertexts with same keystream
# ct1 XOR ct2 = pt1 XOR pt2 (keystream cancels)

# Method 2: PKCS7 padding analysis
# Last byte of plaintext is padding value (0x01 to 0x10)
# Try each padding value to extend keystream recovery

def try_padding(ciphertext, keystream_known):
    """Try to recover more keystream using PKCS7 padding"""
    block_size = 16
    ct_len = len(ciphertext)
    
    # Try padding values 1-16
    for pad_len in range(1, block_size + 1):
        # Padding bytes are all equal to pad_len
        padding = bytes([pad_len] * pad_len)
        
        # Position of padding in ciphertext
        pad_start = ct_len - pad_len
        
        # Recover keystream at padding position
        ks_at_pad = xor_bytes(ciphertext[pad_start:], padding)
        
        # Verify consistency with known keystream
        # (keystream repeats with counter, need to check alignment)
        
        print(f"Padding {pad_len}: keystream = {ks_at_pad.hex()}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Full solution with complete keystream recovery
def solve(ciphertext, known_prefix):
    # Recover keystream progressively
    keystream = bytearray(len(ciphertext))
    
    # From known prefix
    for i, (c, p) in enumerate(zip(ciphertext, known_prefix)):
        keystream[i] = c ^ p
    
    # Extend using CTR counter structure and padding
    # ... (task-specific logic)
    
    # Decrypt
    plaintext = xor_bytes(ciphertext, bytes(keystream))
    return plaintext

# Example with full keystream (if recovered)
# keystream = bytes.fromhex("...")
# plaintext = xor_bytes(ciphertext, keystream)
# print(plaintext.decode())

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering ctr_nonce_reuse_attack, keystream_recovery, known_plaintext_attack was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-03-09 • CUSTOM • Noncense • Difficulty: Medium