Red Hearing
Challenge
Content preserved from the original writeup source. Minimal normalization was applied to fit platform format.
Solution
Original Writeup Content (Preserved)
Red Hearing (280) - Comprehensive Writeup
Challenge artifact
bluehen.jpg
Executive summary
This challenge is intentionally layered:
- The file is malformed as a JPEG (header bytes missing) to break naive tooling.
- A visible clue is stored in a JPEG comment as base64.
- That decoded text is a decoy flag string and also the passphrase for a hidden
steghidepayload. - Extracting the second stage yields the real flag.
Real flag:
UDCTF{l0l_y0u_607_7r1ck3d}
Beginner-friendly explanation
What is steganography?
Steganography means hiding data inside other normal-looking data.
In this challenge:
- The normal-looking file is an image (
bluehen.jpg). - Hidden data appears in two layers:
- A text clue inside JPEG metadata (a comment field).
- Another secret payload embedded with
steghide.
Unlike encryption (which makes data unreadable), steganography tries to make hidden data hard to notice at all.
What is steghide?
steghide is a tool that can hide and extract files/messages inside image or audio carriers.
Important idea:
steghideusually requires a passphrase.- If the passphrase is wrong, extraction fails even if hidden data is present.
In this challenge, the passphrase was the base64 string from the JPEG comment.
Why was this challenge confusing?
It used deliberate misdirection:
- The JPEG was malformed on purpose (missing header bytes), so many tools broke.
- The first decoded text looked like a valid flag, but it was a decoy.
- The decoy was actually a hint toward the real extraction path.
Glossary (quick)
- JPEG comment (COM): A metadata field in JPEG where text can be stored.
- Base64: A text encoding for binary data using letters/numbers/symbols.
- SOI marker: Start Of Image marker (
ff d8) for JPEG. - Payload: The hidden secret content you want to recover.
- Passphrase: Secret text used by steghide to unlock embedded data.
Full investigation flow
1) Initial triage
file bluehen.jpg
exiftool bluehen.jpg
strings -n 6 bluehen.jpg | head -n 120
Why these commands:
file: quick format identification.exiftool: reads metadata where authors often hide hints.strings: pulls readable text from binary files.
Key findings:
filereports genericdata, not a valid JPEG.exiftoolreportsFile format error.stringsshows:
*VURDVEZ7eTB1X2wxazNfcjNkX2gzNHIxbjY/fQ==
Decode it:
echo 'VURDVEZ7eTB1X2wxazNfcjNkX2gzNHIxbjY/fQ==' | base64 -d
Why this command:
- The clue looked like base64 (character set and trailing
==). - Decoding confirms whether it is meaningful text.
Output:
UDCTF{y0u_l1k3_r3d_h34r1n6?}
At this point this looks like a flag, but it is a red herring.
2) Prove where the clue is embedded
xxd -l 128 bluehen.jpg
Why this command:
xxdshows raw bytes, which helps prove exactly where the clue is stored.
Relevant bytes:
... ff fe 00 2a 56 55 52 44 56 45 5a ... 3d 3d ff db ...
Interpretation:
ff fe= JPEG COM segment marker00 2a= segment length- payload bytes = ASCII base64 text
3) Handle the malformed JPEG header
The original file starts with e0 00 10 4a 46 49 46 ... and is missing the normal JPEG prefix ff d8 ff.
Repair script used:
from pathlib import Path
b = Path("bluehen.jpg").read_bytes()
Path("fixed.jpg").write_bytes(bytes.fromhex("ffd8ff") + b)
Validate repaired file:
file fixed.jpg
exiftool fixed.jpg
Why this validation:
- Confirms header repair worked.
- Ensures downstream tools treat
fixed.jpgas a proper JPEG.
Expected:
fixed.jpgis now recognized as JPEG.- It still contains the same comment payload.
4) Extract hidden steghide payload (second stage)
Local machine did not have steghide, so extraction was done in Docker.
Command used:
docker run --rm -v "$PWD":/work -w /work kalilinux/kali-rolling bash -lc \
"apt-get update -y >/dev/null && apt-get install -y steghide >/dev/null && \
steghide extract -sf fixed.jpg \
-p 'VURDVEZ7eTB1X2wxazNfcjNkX2gzNHIxbjY/fQ==' \
-xf /work/extractions/steghide_out.bin -f"
Why Docker here:
- Local environment did not provide
steghide. - Docker gave a clean Linux environment where
steghidecould be installed and run immediately.
How to read the important steghide flags:
extract: recover hidden payload.-sf fixed.jpg: stego file (carrier).-p ...: passphrase.-xf ...: output file path.-f: overwrite output if it already exists.
Then inspect extracted file:
xxd extractions/steghide_out.bin
strings -n 1 extractions/steghide_out.bin
Why both commands:
xxdconfirms exact bytes and structure.stringsquickly shows human-readable content (the flag).
Recovered content:
UDCTF{l0l_y0u_607_7r1ck3d}
Code used in solution
A) Main helper parser (solve.py)
#!/usr/bin/env python3
"""Extract and decode a base64 payload hidden in JPEG COM segments."""
from __future__ import annotations
import argparse
import base64
import re
from pathlib import Path
B64_PATTERN = re.compile(rb"[A-Za-z0-9+/]{12,}={0,2}")
def extract_com_segments_fallback(data: bytes) -> list[bytes]:
"""Best-effort COM segment extraction for malformed JPEG-like files."""
comments: list[bytes] = []
i = 0
while i + 4 <= len(data):
if data[i] == 0xFF and data[i + 1] == 0xFE:
seg_len = int.from_bytes(data[i + 2 : i + 4], "big")
start = i + 4
end = start + max(0, seg_len - 2)
if seg_len >= 2 and end <= len(data):
comments.append(data[start:end])
i = end
continue
i += 1
return comments
def extract_jpeg_comments(data: bytes) -> list[bytes]:
comments: list[bytes] = []
# Prefer strict JPEG parsing when the file is well-formed.
if not data.startswith(b"\xff\xd8"):
return extract_com_segments_fallback(data)
i = 2
while i < len(data):
# Find marker prefix.
if data[i] != 0xFF:
i += 1
continue
# Skip fill bytes 0xFF FF ...
while i < len(data) and data[i] == 0xFF:
i += 1
if i >= len(data):
break
marker = data[i]
i += 1
# Standalone markers without a length field.
if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
continue
if i + 2 > len(data):
break
seg_len = int.from_bytes(data[i : i + 2], "big")
i += 2
if seg_len < 2 or i + (seg_len - 2) > len(data):
break
seg_data = data[i : i + (seg_len - 2)]
i += seg_len - 2
# COM marker
if marker == 0xFE:
comments.append(seg_data)
return comments
def decode_hidden_payload(comments: list[bytes]) -> list[str]:
decoded: list[str] = []
for comment in comments:
for candidate in B64_PATTERN.findall(comment):
try:
text = base64.b64decode(candidate, validate=True).decode("utf-8")
except Exception:
continue
decoded.append(text)
return decoded
def main() -> None:
parser = argparse.ArgumentParser(description="Solve red_hearing stego challenge")
parser.add_argument("image", nargs="?", default="bluehen.jpg", help="Path to JPEG file")
args = parser.parse_args()
data = Path(args.image).read_bytes()
comments = extract_jpeg_comments(data)
if not comments:
print("No JPEG COM segments found.")
return
print(f"Found {len(comments)} COM segment(s).")
decoded = decode_hidden_payload(comments)
if not decoded:
print("No valid base64 payload decoded from comments.")
return
print("Decoded payload(s):")
for item in decoded:
print(item)
print("\nNote: For this challenge, the decoded base64 comment is a steghide passphrase for the repaired JPEG (fixed.jpg).")
if __name__ == "__main__":
main()
Run:
python3 solve.py bluehen.jpg
B) Header repair snippet
from pathlib import Path
b = Path("bluehen.jpg").read_bytes()
Path("fixed.jpg").write_bytes(bytes.fromhex("ffd8ff") + b)
C) Optional passphrase brute script used in container
cat > /tmp/passlist.txt <<'EOF'
UDCTF{y0u_l1k3_r3d_h34r1n6?}
y0u_l1k3_r3d_h34r1n6?
VURDVEZ7eTB1X2wxazNfcjNkX2gzNHIxbjY/fQ==
redherring
red_herring
red hearing
r3d_h3rr1n6
bluehen
bluehens
UDCTF
udctf
EOF
docker run --rm -v "$PWD":/work -v /tmp/passlist.txt:/tmp/passlist.txt -w /work kalilinux/kali-rolling bash -lc '
apt-get update -y >/dev/null && apt-get install -y steghide >/dev/null
set +e
while IFS= read -r p; do
echo "TRY:$p"
steghide extract -sf fixed.jpg -p "$p" -xf /tmp/out.bin -f >/tmp/steg.log 2>&1
if [ $? -eq 0 ]; then
echo "SUCCESS:$p"
cp /tmp/out.bin /work/extractions/steghide_out.bin
exit 0
fi
done < /tmp/passlist.txt
exit 1
'
Intricacies and pitfalls
-
Malformed carrier trick: The challenge file is intentionally missing JPEG SOI bytes, causing some tools to fail or mis-detect format.
-
Decoy flag string: The first decoded value (
UDCTF{y0u_l1k3_r3d_h34r1n6?}) is not accepted as final. It is both thematic bait and a clue. -
Passphrase indirection: The actual
steghidepassphrase is not the decoded plaintext. It is the base64 string itself from the comment. -
Tooling environment issue:
steghidewas unavailable via local package manager, so containerized execution was required. -
Why many brute-force image transforms failed: The payload is not in plain pixel LSB planes; it is in steghide embedding space (JPEG domain), so visual channel tricks alone are insufficient.
-
Practical lesson: If you see a suspicious metadata clue that looks "too easy", treat it as either a decoy or a key for the next stage.
-
Practical lesson: For malformed media files, repair minimum structure first (headers/markers), then re-run standard tooling.
Final flag
UDCTF{l0l_y0u_607_7r1ck3d}