← Back to Writeups
CUSTOMMediumCrypto

Hell is haard

XESXOR9/10/20264 min read
#crypto#custom#medium#blockwise_byte_shift_recovery#inverse_haar_transform#recursive_signal_reconstructio

Hell is haard

Platform: bluehensctf | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-22 | Status: Solved Techniques: blockwise_byte_shift_recovery, inverse_haar_transform, recursive_signal_reconstruction

Summary

Task: a JSON array of 2048 integers hides a message behind a Haar-based encoding and extra byte scrambling. Solution: recursively apply the inverse unscaled Haar transform, convert the recovered samples to bytes, then fix each 512-byte block with the correct modulo-256 rotation to reveal the flag.

Hell is haard — BlueHens CTF 2026

Description

My good friend dante is trying to send me a message from the deepest level of hell but it its all convoluted!

We are given message.txt, which looks like raw numeric data instead of ciphertext or natural language. The goal is to recover the hidden plaintext and extract the embedded flag.

Challenge Summary

The file contains a JSON list of 2048 integers. The challenge title is the real hint: haard points to the Haar transform. Reversing the unscaled Haar encoding reconstructs 2048 plaintext bytes, and three of the four 512-byte blocks then need an additional modulo-256 byte rotation to become readable English. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Recon / Observations

The first useful step is simply inspecting message.txt. It is valid JSON containing 2048 signed integers, which strongly suggests transformed signal data rather than a standard text cipher.

Two details stand out:

  1. The challenge name uses haard, which is very likely a pun on the Haar wavelet transform.
  2. The data length is a power of two, which fits recursive transforms perfectly.

That makes an inverse Haar reconstruction the natural thing to try.

Why the Haar Hint Matters

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

In an unscaled Haar transform, each stage stores averages and details. If a block contains average a and detail d, the original pair is recovered as:

text
left  = (a + d) / 2
right = (a - d) / 2

The encoded data stores these coefficients recursively. Starting from the smallest prefix and repeatedly expanding it with that rule reconstructs the full 2048-sample sequence.

Without noticing the Haar hint, the integer list just looks arbitrary. Once the title is interpreted correctly, the recovery path becomes straightforward. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Applying the inverse unscaled Haar transform recursively yields floating-point values very close to byte values. Rounding them and reducing modulo 256 produces a 2048-byte plaintext candidate.

That plaintext splits naturally into four 512-byte blocks:

  • block 0: already readable English
  • block 1: readable after byte shift +90 mod 256
  • block 2: readable after byte shift +45 mod 256
  • block 3: readable after byte shift +138 mod 256

These shifts act like an 8-bit Caesar cipher applied independently to each block. After correcting them, the text becomes a Dante excerpt containing the flag string: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

text
udctf{REDACTED}

Solution

  1. Parse message.txt as JSON.
  2. Treat the array as recursively Haar-encoded coefficients.
  3. Rebuild the plaintext samples by repeatedly expanding the recovered prefix:
    • take averages from the first half of the current prefix
    • take details from the second half
    • recover each original pair with (a + d) / 2 and (a - d) / 2
  4. After all 2048 samples are restored, round each value and reduce modulo 256.
  5. Split the byte string into four 512-byte blocks.
  6. Apply per-block rotations of 0, 90, 45, and 138 modulo 256.
  7. Read the recovered Dante passage and extract the embedded flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/usr/bin/env python3
import json
from pathlib import Path

coeffs = json.loads(Path("tasks/BlueHens CTF 2026/Hell is haard/message.txt").read_text())

prefix = [coeffs[0]]
size = 1
while size < len(coeffs):
    details = coeffs[size:2 * size]
    rebuilt = []
    for a, d in zip(prefix, details):
        rebuilt.append((a + d) / 2)
        rebuilt.append((a - d) / 2)
    prefix = rebuilt
    size *= 2

data = bytes(round(x) % 256 for x in prefix)
shifts = [0, 90, 45, 138]

parts = []
for i, shift in enumerate(shifts):
    block = data[i * 512:(i + 1) * 512]
    parts.append(bytes((b + shift) % 256 for b in block))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

plaintext = b"".join(parts).decode("latin1")
print(plaintext)

The decoded text is a long English passage from Dante, and one sentence contains the flag verbatim.

Short Takeaways

  • A challenge title that sounds slightly wrong is often a direct hint.
  • Power-of-two numeric arrays are a strong sign of recursive transforms.
  • If recovered text is nearly readable, test simple post-processing like blockwise byte rotations. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍