This Saturday began with a familiar domestic scene: an interactive optical pen resting on the living room table surrounded by illustrated children's books. For those unfamiliar with these gadgets, they look like chunky plastic styluses that instantly play voice lines or sound effects when tapping their tip against any illustration or sentence on the printed page.

To a child's eyes, it looks like magic. To an engineer's eyes, it is a tiny infrared camera in the tip scanning microscopic 2D dot patterns (OID, Optical Identification) and triggering audio clips stored on internal flash memory.

Nothing out of the ordinary so far. The trigger for my curiosity arrived when I noticed how new books were synchronized. Instead of requiring a USB cable, the pen syncs content via Bluetooth from a mobile app. Watching that wireless progress bar transfer several megabytes of data immediately sparked the itch to tinker: what format travels over the air?, is the audio encrypted?, can we extract the raw files to play them anywhere?

Coffee in hand and with the afternoon ahead, I set out to investigate.

1. Packet Sniffing: Bluetooth HCI Snoop Log to the Rescue

The first step was intercepting the file payload during transmission. Instead of wrestling with SDR receivers or cumbersome wireless monitoring setups like those we used for Wi-Fi audits, Android 4.4 KitKat introduced a great feature in developer options: Bluetooth HCI snoop log.

When enabled, the phone's Bluetooth stack captures all host controller interface (HCI) packets and writes them directly to local storage.

The capture procedure was straightforward:

  1. Enable the HCI snoop log under developer options.
  2. Open the companion sync app and push a storybook pack to the pen.
  3. Wait for the transfer to complete.
  4. Disable logging and pull the resulting capture file to my laptop using ADB:
adb pull /sdcard/btsnoop_hci.log ./captura_lapiz.log

Opening captura_lapiz.log in Wireshark and filtering by RFCOMM protocol and OBEX transfers revealed the full transaction: an initial serial channel handshake followed by a steady stream of packets transferring a binary bundle of nearly 14 megabytes named pack_story_03.dat.

Using Wireshark's Export Packet Bytes option, I dumped the raw stream straight to disk. I had the binary package.

2. Binary Dissection: Spotting Patterns in the Hex Dump

Attempting to open pack_story_03.dat in media players or standard archive utilities failed with an unrecognized format error. Time to launch the terminal and run xxd:

xxd -g 1 pack_story_03.dat | head -n 30

The first thirty lines displayed an interesting layout:

00000000: 4f 49 44 50 01 00 00 00 58 02 00 00 e4 0a d2 00  OIDP....X.......
00000010: a3 5f 12 7b c8 44 91 02 a3 5f 12 7b c8 44 91 02  ._.{.D..._.{.D..
00000020: 3a 9c f0 1e 54 b2 88 e9 3a 9c f0 1e 54 b2 88 e9  :...T...:...T...
00000030: a3 5f 12 7b c8 44 91 02 a3 5f 12 7b c8 44 91 02  ._.{.D..._.{.D..

Examining the dump revealed several immediate clues:

  • Recognizable header: The initial 4 bytes read OIDP (ASCII for Optical Identification Package).
  • Metadata fields: Bytes 0x04 through 0x07 represent format version (0x00000001), followed by 32-bit little-endian integers indicating index count and total container size.
  • Suspicious entropy with periodic rhythm: Starting at offset 0x0010, identical 8-byte sequences repeated verbatim across adjacent blocks.

When a binary payload displays recurring sequences in adjacent regions rather than uniform white-noise randomness, you are looking at simple byte masking rather than modern block ciphers with initialization vectors like AES-CBC.

Budget microcontrollers inside children's toys prioritize battery life and CPU thermal limits; they cannot afford heavy decryption cycles in real time while a child drags the tip across paper.

3. Breaking the Mask: Exploiting Predictable Zero Bytes

In almost every binary container featuring partition tables or sector alignment, padding areas filled entirely with null bytes (0x00) are standard.

The XOR operation has a clean arithmetic identity:

$$X \oplus 0 = X$$

If a packaging tool masks a null-padded block by applying a cyclical XOR key, the resulting bytes stored in the container are the raw key bytes repeated sequentially.

Looking at lines 0x0010 and 0x0030, the sequence a3 5f 12 7b c8 44 91 02 was repeated. Inspecting the end of the header block, where 512-byte sector alignment padding typically sits, confirmed the layout.

The key was a repeating 16-byte mask:

\xa3\x5f\x12\x7b\xc8\x44\x91\x02\xd4\x11\x8e\x33\xf9\xaa\x45\x60

Applying this mask through a rolling XOR loop starting at offset 0x0040 cleared the haze immediately.

A clean table of contiguous records emerged. Each index entry occupied exactly 16 bytes:

  • 2 bytes (uint16 little-endian): OID coordinate or page code.
  • 2 bytes (uint16 little-endian): Track or sentence identifier.
  • 4 bytes (uint32 little-endian): Absolute file offset.
  • 4 bytes (uint32 little-endian): Audio stream length in bytes.
  • 4 bytes: CRC32 checksum of the audio segment.

Jumping directly to the first offset specified by the table uncovered the header of the first audio chunk:

ff fb 90 64 00 00 00 00 ...

That 0xFF 0xFB signature is the sync word for an MPEG-1 Audio Layer 3 (MP3) frame running at 128 kbps and 44.1 kHz.

4. The Extractor: Automating the Dump with Python

With the binary layout mapped out, I wrote a Python script to parse the container, strip the XOR mask, read the allocation table, and extract individual MP3 files alongside a JSON manifest linking each OID code to its corresponding audio track.

#!/usr/bin/env python3
"""
Resource extractor for interactive optical pen bundles.
Parses the container, strips the cyclical XOR mask, and dumps MP3 tracks.
"""

import json
import struct
import sys
from pathlib import Path

# Repeating 16-byte mask identified in the dump
XOR_KEY = bytes([
    0xA3, 0x5F, 0x12, 0x7B, 0xC8, 0x44, 0x91, 0x02,
    0xD4, 0x11, 0x8E, 0x33, 0xF9, 0xAA, 0x45, 0x60
])


def deobfuscate_block(buffer: bytes, key: bytes, initial_offset: int = 0) -> bytes:
    """Applies the rolling XOR mask accounting for absolute byte offset."""
    result = bytearray(len(buffer))
    key_len = len(key)
    for i, byte in enumerate(buffer):
        key_byte = key[(initial_offset + i) % key_len]
        result[i] = byte ^ key_byte
    return bytes(result)


def extract_package(source_file: Path, output_dir: Path) -> None:
    output_dir.mkdir(parents=True, exist_ok=True)

    with open(source_file, "rb") as f:
        data = f.read()

    # Verify OIDP magic bytes
    magic = data[:4]
    if magic != b"OIDP":
        raise ValueError(f"Invalid container signature: {magic}")

    version, total_entries, table_offset = struct.unpack_from("<III", data, 4)
    print(f"Container version: {version}")
    print(f"Indexed audio clips: {total_entries}")
    print(f"Allocation table offset: 0x{table_offset:04X}")

    manifest = []
    table_cursor = table_offset
    record_size = 16

    for i in range(total_entries):
        # Read and deobfuscate index record
        index_chunk = data[table_cursor : table_cursor + record_size]
        plain_index = deobfuscate_block(index_chunk, XOR_KEY, table_cursor)

        oid_code, phrase_id, audio_offset, audio_len = struct.unpack("<HHII", plain_index[:12])
        table_cursor += record_size

        # Extract and descramble MP3 payload
        scrambled_payload = data[audio_offset : audio_offset + audio_len]
        clean_mp3 = deobfuscate_block(scrambled_payload, XOR_KEY, audio_offset)

        # Check MP3 sync frame
        if clean_mp3[:2] != b"\xff\xfb" and clean_mp3[:3] != b"ID3":
            print(f"Warning: Offset 0x{audio_offset:X} lacks classic MP3 sync header.")

        filename = f"oid_{oid_code:05d}_track_{phrase_id:02d}.mp3"
        dest_path = output_dir / filename

        with open(dest_path, "wb") as f_out:
            f_out.write(clean_mp3)

        manifest.append({
            "oid_code": oid_code,
            "phrase_id": phrase_id,
            "file": filename,
            "bytes": audio_len
        })

    # Save manifest linking physical coordinates to media
    with open(output_dir / "book_index.json", "w", encoding="utf-8") as f_json:
        json.dump(manifest, f_json, indent=2)

    print(f"Finished: {len(manifest)} audio tracks dumped to {output_dir}/")


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"Usage: python {sys.argv[0]} <bundle.dat> <output_folder>")
        sys.exit(1)

    extract_package(Path(sys.argv[1]), Path(sys.argv[2]))

Running the script against pack_story_03.dat:

python extractor.py pack_story_03.dat ./extracted_audio/

Within seconds, the destination folder contained 84 .mp3 files playable in mplayer or any media player, alongside a complete JSON index. Voice recordings, background tunes, and dialogues were intact with pristine sound quality.

5. Why Inspecting Personal Hardware Matters

Gaining direct access to the audio assets and OID mapping table opens practical options beyond reliance on the original plastic toy:

  1. Durable backups: If the stylus breaks or its non-replaceable lithium cell degrades, educational book recordings remain preserved.
  2. Digital indexing: Matching book_index.json against vocabulary transcripts makes it easy to build desktop or web flashcard tools without carrying physical hardware.
  3. Understanding consumer hardware: Most interactive learning toys rely on standard low-power chipsets driven by localized optical events, shielding their media assets behind minimal rolling-byte obfuscation.

There is enduring satisfaction in taking an afternoon to dissect an opaque proprietary format and converting a locked byte stream into open, documented information.