Skip to main content

Blind Hens

Challenge

Content preserved from the original writeup source. Minimal normalization was applied to fit platform format.

Solution

Original Writeup Content (Preserved)

Blind Hens (100) - Comprehensive Writeup

Challenge Summary

We are given a routine internal memo where the visible text appears normal, but the prompt hints that:

  • The hidden message is "in plain sight"
  • "Smallest details" matter
  • "Internal policies" may be a key

These clues strongly suggest steganography in formatting details, not visible wording.

Initial Recon

The file provided was:

  • memo.txt

A visual read already showed suspicious spacing at the ends of some lines. The strongest signal was inconsistent trailing whitespace (spaces and tabs), especially:

  • In selected operational bullet lines near the top
  • In policy appendix notes around 100-147
  • Around signature/footer lines

Given the clue about tiny details, trailing whitespace is the covert channel.

Core Hypothesis

Use trailing whitespace as binary:

  • space -> 0
  • tab -> 1

Then decode the resulting bitstream.

Evidence Collection

Trailing whitespace was made visible with a control-character view (for example using cat -vet or raw byte inspection). This confirmed that selected lines end with mixed tabs and spaces.

A scripted extraction of only trailing whitespace patterns showed fixed-width groups, mostly 8 symbols long, and one final 6-symbol tail at EOF.

That tail is important: it indicates the message is not simply byte-aligned data; it likely includes a base64-style 6-bit segment at the end.

Decoding Pipeline

Step 1: Extract trailing whitespace from all lines that have it

Take each line's trailing run of [space/tab] characters only.

Step 2: Convert to bits

Map each character:

  • space = 0
  • tab = 1

Concatenate all groups in file order.

Step 3: Interpret as 6-bit symbols

Because one trailing segment is length 6, decode the stream as 6-bit values over the base64 alphabet.

This reconstructs the string: ZG1kZ2QyVllFMDRWZkZSTEVoUVFmQlpURjBBUWZFY1FRQk5IRWswVmZCSVdmQmQ4RkVzU1RSVWNYZz3

The missing end aligns with the final 6-bit segment that maps to '=' padding, giving:

dmdgd2VYE04VfFRLEhQQfBZTF0AQfEcQQBNHEk0VfBIWfBd8FEsSTRUcXg==

Step 4: Base64 decode

Decoding that yields binary beginning with: vg`weX... (non-printable mixed bytes)

So there is another transformation layer.

Step 5: Apply policy clue

The memo explicitly says: "Oh btw please ensure compliance with policy #23"

Use policy 23 as XOR key byte:

  • XOR each decoded byte with 0x23

This cleanly reveals readable plaintext: UDCTF{0m6_wh173_5p4c3_d3c0d1n6_15_4_7h1n6?}

Final Flag

UDCTF{0m6_wh173_5p4c3_d3c0d1n6_15_4_7h1n6?}

Why This Works

The challenge layers multiple hints:

  1. "Smallest details" -> trailing whitespace stego
  2. Lots of policy lines + explicit "policy #23" -> decryption key/value 23
  3. Mixed 8-bit and final 6-bit chunk -> base64-related reconstruction before final XOR

Repro Script (Minimal)

from pathlib import Path
import base64

alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

text = Path("memo.txt").read_text().splitlines()

# collect trailing whitespace groups in order
trails = []
for line in text:
t = line[len(line.rstrip(" \t")):]
if t:
trails.append(t)

# map space/tab to bits and concatenate
bits = "".join("".join("1" if c == "\t" else "0" for c in t) for t in trails)

# decode as 6-bit base64 symbols
n = len(bits) // 6
b64_stage1 = "".join(alpha[int(bits[i*6:(i+1)*6], 2)] for i in range(n))

# the final 6-bit tail in file maps to '=' padding
b64_stage2 = b64_stage1 + "="

raw = base64.b64decode(b64_stage2)
flag = bytes(x ^ 0x23 for x in raw).decode("ascii")
print(flag)
#!/usr/bin/env python3
from pathlib import Path
import base64

ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


def trailing_ws(line: str) -> str:
return line[len(line.rstrip(" \t")):]


def ws_to_bits(ws: str) -> str:
return "".join("1" if ch == "\t" else "0" for ch in ws)


def decode_whitespace_stream(path: Path):
lines = path.read_text().splitlines()
trails = [trailing_ws(line) for line in lines]
trails = [t for t in trails if t]

bitstream = "".join(ws_to_bits(t) for t in trails)
sextet_count = len(bitstream) // 6
remainder = len(bitstream) % 6

stage1 = "".join(
ALPHA[int(bitstream[i * 6:(i + 1) * 6], 2)]
for i in range(sextet_count)
)

# This challenge stores a final 6-bit tail that corresponds to '=' padding.
# Keep behavior explicit for reproducibility.
stage2 = stage1 + "="

return {
"line_count": len(lines),
"trailing_groups": len(trails),
"bit_len": len(bitstream),
"bit_remainder": remainder,
"stage1": stage1,
"stage2": stage2,
}


def recover_flag(stage2: str) -> str:
raw = base64.b64decode(stage2)
plain = bytes(b ^ 0x23 for b in raw)
return plain.decode("ascii")


def main():
target = Path("memo.txt")
if not target.exists():
raise FileNotFoundError("memo.txt not found in current directory")

data = decode_whitespace_stream(target)

print("[+] trailing groups:", data["trailing_groups"])
print("[+] bit length:", data["bit_len"])
print("[+] bit remainder:", data["bit_remainder"])
print("[+] stage1:", data["stage1"])
print("[+] stage2:", data["stage2"])

flag = recover_flag(data["stage2"])
print("[+] flag:", flag)


if __name__ == "__main__":
main()

Example run:

[+] trailing groups: 60
[+] bit length: 478
[+] bit remainder: 4
[+] stage1: ZG1kZ2QyVllFMDRWZkZSTEVoUVFmQlpURjBBUWZFY1FRQk5IRWswVmZCSVdmQmQ4RkVzU1RSVWNYZz3
[+] stage2: ZG1kZ2QyVllFMDRWZkZSTEVoUVFmQlpURjBBUWZFY1FRQk5IRWswVmZCSVdmQmQ4RkVzU1RSVWNYZz3=
[+] flag: UDCTF{0m6_wh173_5p4c3_d3c0d1n6_15_4_7h1n6?}

Notes

  • If your local text editor auto-trims trailing whitespace, this challenge will break immediately.
  • Preserve the original file bytes when investigating stego challenges.