Skip to main content

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:

  1. The file is malformed as a JPEG (header bytes missing) to break naive tooling.
  2. A visible clue is stored in a JPEG comment as base64.
  3. That decoded text is a decoy flag string and also the passphrase for a hidden steghide payload.
  4. 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:
    1. A text clue inside JPEG metadata (a comment field).
    2. 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:

  • steghide usually 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:

  • file reports generic data, not a valid JPEG.
  • exiftool reports File format error.
  • strings shows:
*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:

  • xxd shows 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 marker
  • 00 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.jpg as a proper JPEG.

Expected:

  • fixed.jpg is 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 steghide could 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:

  • xxd confirms exact bytes and structure.
  • strings quickly 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

  1. Malformed carrier trick: The challenge file is intentionally missing JPEG SOI bytes, causing some tools to fail or mis-detect format.

  2. 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.

  3. Passphrase indirection: The actual steghide passphrase is not the decoded plaintext. It is the base64 string itself from the comment.

  4. Tooling environment issue: steghide was unavailable via local package manager, so containerized execution was required.

  5. 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.

  6. 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.

  7. Practical lesson: For malformed media files, repair minimum structure first (headers/markers), then re-run standard tooling.

Final flag

UDCTF{l0l_y0u_607_7r1ck3d}