│ Category: Cryptography
│ Difficulty: Hard
│ Points: 400
│ Author: Imattas aka Zemi
────────────────────────────────────────────────────────────────────────────────
--[ Challenge Description ]--
│ no. no. that's Not TRUe. that's impossible!
────────────────────────────────────────────────────────────────────────────────
--[ Recon / Initial Analysis ]--
We are given a remote service (nc) that implements a custom authentication
scheme. The server uses a truncated hash/MAC to verify messages. The name "Not
TRUe" hints at a truncation attack -- where a cryptographic hash or MAC is
truncated to fewer bits, making it feasible to brute-force a valid tag.
The challenge name "Not TRUe" is a play on "TRU" / truncation. In proper
cryptographic implementations, MACs and hashes use their full output length
(e.g., 256 bits for SHA-256). However, when the output is truncated to a small
number of bits (e.g., 16-32 bits), it becomes computationally trivial to forge a
valid tag through brute force.
The typical server interaction flow is:
1. Connect to the server
2. Server provides a challenge or expects a message + tag pair
3. We craft a message and brute-force the truncated tag
4. Submit the forged message to retrieve the flag
────────────────────────────────────────────────────────────────────────────────
--[ Vulnerability / Observation ]--
1. Truncated MAC/Hash: The server computes a MAC or hash over user-supplied data
but only checks a truncated portion (e.g., the first 2-4 bytes). This
drastically reduces the search space from 2^256 to 2^16 or 2^32.
2. Attack Strategy: We can forge a message with a valid truncated tag by:
- Observing the server's behavior and understanding the truncation length
- Brute-forcing the truncated portion by trying random inputs until one
produces a matching truncated hash
- Alternatively, if the server uses HMAC-SHA256 truncated to N bits, we
generate candidate messages and check if the first N bits of their hash match
what the server expects
3. Birthday-style collision: With a 16-bit truncation, we only need ~2^8 = 256
attempts on average (birthday bound) or at most 2^16 = 65536 attempts for a
guaranteed match against a fixed target.
────────────────────────────────────────────────────────────────────────────────
--[ Exploitation / Solution ]--
1. Connect to the challenge server using netcat or pwntools
2. Parse the server's response to understand the expected format
3. Brute-force the truncated hash/MAC by iterating through possible values
4. Send the forged message + valid truncated tag
5. Receive the flag
────────────────────────────────────────────────────────────────────────────────
--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Not TRUe - picoCTF 2026 (Cryptography, 400 pts)
Truncation attack on a cryptographic MAC/hash.
The server uses a truncated hash to verify authenticity, making it
feasible to brute-force a valid tag.
Usage:
python3 solve.py [HOST] [PORT]
Adjust HOST and PORT to match the challenge instance.
"""
import hashlib
import itertools
import struct
import sys
from pwn import *
# ============================================================
# Configuration - update these with the actual challenge values
# ============================================================
HOST = sys.argv[1] if len(sys.argv) > 1 else "rescued-float.picoctf.net"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 1337
# Truncation length in bytes (common values: 2 = 16-bit, 3 = 24-bit, 4 = 32-bit)
# Adjust based on what the challenge server uses
TRUNC_BYTES = 2
def compute_truncated_hash(data: bytes, trunc_len: int = TRUNC_BYTES) -> bytes:
"""Compute SHA-256 hash and truncate to trunc_len bytes."""
full_hash = hashlib.sha256(data).digest()
return full_hash[:trunc_len]
def compute_truncated_hmac(key: bytes, data: bytes, trunc_len: int = TRUNC_BYTES) -> bytes:
"""Compute HMAC-SHA256 and truncate to trunc_len bytes."""
import hmac
full_mac = hmac.new(key, data, hashlib.sha256).digest()
return full_mac[:trunc_len]
def brute_force_truncated_hash(target_tag: bytes, prefix: bytes = b"", trunc_len: int = TRUNC_BYTES):
"""
Brute-force a message whose truncated SHA-256 hash matches target_tag.
Appends different suffixes to prefix until the truncated hash matches.
"""
log.info(f"Brute-forcing truncated hash ({trunc_len} bytes = {trunc_len*8} bits)...")
for i in range(2 ** (trunc_len * 8 + 4)): # Generous upper bound
candidate = prefix + struct.pack("<I", i)
h = hashlib.sha256(candidate).digest()[:trunc_len]
if h == target_tag:
log.success(f"Found collision after {i+1} attempts")
return candidate
return None
def brute_force_message_for_tag(target_tag_hex: str, trunc_len: int = TRUNC_BYTES):
"""
Given a hex-encoded target tag, find a message whose truncated hash matches.
"""
target_tag = bytes.fromhex(target_tag_hex)
return brute_force_truncated_hash(target_tag, b"forge_", trunc_len)
def solve():
"""
Main solve routine. Connects to the server and exploits the truncated
hash/MAC to forge a valid authentication token.
"""
conn = remote(HOST, PORT)
# ============================================================
# Phase 1: Read the server banner and understand the protocol
# ============================================================
banner = conn.recvuntil(b"\n", timeout=5)
log.info(f"Banner: {banner.decode().strip()}")
# Read all available initial data
try:
initial_data = conn.recvuntil(b"\n", timeout=3)
log.info(f"Server says: {initial_data.decode().strip()}")
except:
pass
# ============================================================
# Phase 2: Interact with the server
# The general approach for truncation attacks:
# - Server sends a challenge or asks for message + tag
# - We brute-force the truncated portion
# ============================================================
# Strategy A: If server asks us to provide a message with valid tag
# We try random messages until one has the right truncated hash
log.info("Attempting truncation brute-force attack...")
# Generate candidate messages and try them
for attempt in range(65536):
# Craft a candidate message
msg = f"admin:{attempt}".encode()
tag = hashlib.sha256(msg).hexdigest()[:TRUNC_BYTES * 2] # hex-encoded truncated hash
# Send to server (adjust format based on actual protocol)
try:
conn.sendline(msg.hex() + ":" + tag)
except:
break
response = conn.recvline(timeout=2)
resp_str = response.decode().strip() if response else ""
if "picoCTF{" in resp_str:
log.success(f"Flag found: {resp_str}")
print(resp_str)
conn.close()
return
if "correct" in resp_str.lower() or "success" in resp_str.lower() or "flag" in resp_str.lower():
log.success(f"Possible success: {resp_str}")
# Try to receive more data that might contain the flag
try:
more = conn.recvall(timeout=3)
full_response = resp_str + more.decode()
if "picoCTF{" in full_response:
flag_start = full_response.index("picoCTF{")
flag_end = full_response.index("}", flag_start) + 1
flag = full_response[flag_start:flag_end]
log.success(f"Flag: {flag}")
print(flag)
except:
pass
conn.close()
return
if attempt % 1000 == 0 and attempt > 0:
log.info(f"Tried {attempt} candidates...")
# ============================================================
# Strategy B: If we need to forge a specific message
# Server gives us a known-good (message, tag) pair and we need
# to produce a *different* message with the same truncated tag
# ============================================================
log.warning("Strategy A did not work. The protocol may differ from what was assumed.")
log.info("Please examine the server's actual protocol and adjust the script accordingly.")
log.info("Key variables to adjust: TRUNC_BYTES, message format, and tag computation.")
conn.close()
def offline_demo():
"""
Demonstrate the truncation attack offline.
Shows how easy it is to find collisions with truncated hashes.
"""
log.info("=== Offline Truncation Attack Demo ===")
original_msg = b"hello world"
full_hash = hashlib.sha256(original_msg).hexdigest()
trunc_hash = full_hash[:TRUNC_BYTES * 2]
log.info(f"Original message: {original_msg}")
log.info(f"Full SHA-256: {full_hash}")
log.info(f"Truncated ({TRUNC_BYTES} bytes): {trunc_hash}")
# Find a collision
target = bytes.fromhex(trunc_hash)
collision = brute_force_truncated_hash(target, b"forged_", TRUNC_BYTES)
if collision:
collision_hash = hashlib.sha256(collision).hexdigest()[:TRUNC_BYTES * 2]
log.success(f"Forged message: {collision}")
log.success(f"Truncated hash: {collision_hash}")
log.success(f"Match: {trunc_hash == collision_hash}")
if __name__ == "__main__":
if "--demo" in sys.argv:
offline_demo()
else:
solve()
────────────────────────────────────────────────────────────────────────────────
--[ Key Takeaways ]--
- Truncating a hash or MAC to a small output (16-32 bits) destroys its security:
the forgery search space collapses from 2^256 to as little as 2^16.
- A guaranteed match against a fixed target needs at most 2^N tries for an N-bit
tag; a birthday collision needs only ~2^(N/2).
- Truncation attacks combine offline brute-forcing (find a message with the
right truncated tag) with online interaction (submit it to the server).
- pwntools remote() is the standard tool for scripting netcat-style challenge
servers; hashlib handles the hash computation.
- Always use full-length MAC/hash outputs in real systems — never truncate to
"save space."