Martin Rozariyo
Back to writeups

L3m0nCTF2025-Writeups

Challenge Overview: Mystery Call

Forensics/LastCall/Readme.md

Challenge Overview: Mystery Call

Category: Forensics
Event: L3m0nCTF 2025
Role: Challenge Author

πŸ› οΈ Author Note
This challenge was authored by me for L3m0nCTF 2025.
The following explanation describes the intended forensic analysis path.

image

Intended Analysis Path

The challenge was designed to test:

  • recognition of signaling methods beyond direct decoding
  • understanding that DTMF symbols may act as carriers, not data
  • analysis of temporal properties rather than frequency content
  • multi-channel signal combination to remove decoy information

Single-channel or direct decoding approaches were intentionally misleading.

Overview

Files given : mystery_call.tar.gz

We are given a single audio file, mystery_call.wav.

The audio contains nothing except a sequence of repetitive electronic tones. There is no speech, no music, and no obvious audible message.

At first listen, the tones resemble telephone beeps, but decoding them directly does not reveal any readable text.

The goal is to determine what information is hidden inside these tones.

Analysis Phase 1 β€” Initial File Inspection

We begin by checking the file type.

file mystery_call.wav
image

The output confirms:

  • WAV audio
  • Stereo
  • Fixed sample rate

This is important β€” the file has two channels, not one.

Analysis Phase 2 β€” Eliminating Direct DTMF Decoding

Since the tones resemble telephone signals, we try a standard DTMF decoder.

multimon-ng -a DTMF -t wav mystery_call.wav
image

This produces a long stream of same repeated digits.

However:

  • The output does not resemble a flag
  • The sequence is far too long
  • No readable structure appears

This tells us something important:

The decoded DTMF symbols themselves are not the message.

Analysis Phase 3 β€” Channel-Level Inspection

On opening this file in audacity we can see that both left channel and right channel doesnt resemble same sound.

image

So, we separate the channels.

sox mystery_call.wav left.wav  remix 1
sox mystery_call.wav right.wav remix 2

Now we have:

  • left.wav
  • right.wav

Each channel contains valid tones.

Running a DTMF decoder on either channel alone still does not produce meaningful text.

So:

  • The message is not stored in a single channel
  • The channels must be used together

Analysis Phase 4 β€” Identifying the True Signal Property

When visualizing the waveform or spectrogram, one key detail stands out:

  • The frequencies do not change meaningfully
  • The only variation is tone duration

Each tone is either:

  • Short
  • Long

This strongly suggests binary encoding.

We treat:

  • Short tone β†’ 0
  • Long tone β†’ 1

Analysis Phase 5 β€” Binary Extraction via Temporal Analysis

For each channel independently:

1.Convert audio to mono signal

2.Compute short-time energy

3.Detect tone β€œon” segments

4.Measure duration of each segment

5.Convert duration to bits

The extraction logic is implemented in a helper script

extract.py

#!/usr/bin/env python3
import numpy as np
from scipy.io import wavfile

SHORT = 0.10
LONG  = 0.22
THRESH = (SHORT + LONG) / 2
WINDOW = 256

def extract_bits(wav):
    sr, x = wavfile.read(wav)

    # mono safety
    if x.ndim > 1:
        x = x[:,0]

    x = x.astype(np.float32)
    x /= np.max(np.abs(x))

    energy = np.convolve(x*x, np.ones(WINDOW)/WINDOW, mode="same")
    on = energy > (0.2 * energy.max())

    bits = []
    i = 0
    while i < len(on):
        if on[i]:
            j = i
            while j < len(on) and on[j]:
                j += 1
            dur = (j - i) / sr
            bits.append('1' if dur > THRESH else '0')
            i = j
        else:
            i += 1

    return ''.join(bits)

left  = extract_bits("left.wav")
right = extract_bits("right.wav")
print("[LEFT bits]")
print(left)
print("Length:", len(left))

print("\n[RIGHT bits]")
print(right)
print("Length:", len(right))


n = min(len(left), len(right))
xor_bits = ''.join(str(int(left[i]) ^ int(right[i])) for i in range(n))

out = []
for i in range(0, len(xor_bits), 8):
    byte = xor_bits[i:i+8]
    if len(byte) == 8:
        out.append(chr(int(byte, 2)))

print("".join(out))

Running this script produces two binary streams:

This produces two binary strings:

image

Neither stream alone forms valid ASCII.

Analysis Phase 6 β€” Channel Combination via XOR

Because both channels are synchronized and neither decodes meaningfully by itself, the next step is to combine them.

The intended operation is a bitwise XOR between corresponding bits:

FINAL_BIT = LEFT_BIT βŠ• RIGHT_BIT

This operation removes the decoy structure present in each channel individually and reconstructs the original data.

The XOR and decoding process is handled by the that code even:

Final Output

The resulting binary stream is grouped into 8-bit chunks and converted to ASCII.

This yields the final message:

L3m0nCTF{DTMF_X0R_5T3R30_1S_N0T_4UD10_3V3RY0N3}