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:
- "Smallest details" -> trailing whitespace stego
- Lots of policy lines + explicit "policy #23" -> decryption key/value 23
- 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)
Full Solver Script (Recommended)
#!/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.