- Destripando un lápiz óptico infantil: De paquetes Bluetooth a extraer sus audios
- 1. Caza de paquetes: Bluetooth HCI Snoop Log al rescate
- 2. Radiografía del binario: Detectando patrones en el volcado hexadecimal
- 3. Reventando la máscara: La ventaja de los ceros predecibles
- 4. El extractor: Automatizando el volcado con Python
- 5. El valor de inspeccionar nuestros propios dispositivos
Este sábado empezó con una escena doméstica habitual: un lápiz óptico interactivo sobre la mesa del salón y varios libros infantiles alrededor. Para quien no los conozca, son dispositivos con forma de lápiz grueso que, al posar la punta sobre cualquier dibujo o frase de sus libros ilustrados, reproducen al instante la locución correspondiente o efectos sonoros.
A ojos de un niño parece magia. A ojos de un ingeniero, es una cámara infrarroja diminuta en la punta leyendo tramas microscópicas de puntos bidimensionales (patrones OID, Optical Identification) y disparando pistas de audio almacenadas en una memoria flash interna.
Hasta ahí, todo normal. El disparador de mi curiosidad llegó al ver cómo se sincronizaban nuevos libros. En lugar de exigir una conexión USB, el lápiz sincroniza contenidos mediante Bluetooth desde una aplicación móvil. Ver esa barra de progreso inalámbrica mientras transfería varios megabytes de datos despertó de inmediato las ganas de trastear: ¿qué formato viaja por el aire?, ¿vienen los audios cifrados?, ¿podemos extraer los archivos para reproducirlos donde queramos?
Café en mano y con la tarde por delante, decidí averiguarlo.
1. Caza de paquetes: Bluetooth HCI Snoop Log al rescate
El primer paso consistió en interceptar el fichero durante la transmisión. En lugar de pelearme con receptores SDR o configuraciones complejas de monitorización inalámbrica como las que usábamos en auditorías Wi-Fi, Android 4.4 KitKat incluye una opción fantástica en sus ajustes de desarrollador: el Registro de inspección de Bluetooth HCI.
Al activar esta casilla, el subsistema de Bluetooth del teléfono vuelca absolutamente todo el tráfico de la interfaz de control del host (HCI) directamente al almacenamiento local.
El procedimiento fue sencillo:
- Activar el volcado HCI en el móvil.
- Abrir la aplicación de sincronización y enviar un libro de cuentos al lápiz.
- Esperar a que la transferencia completara su recorrido.
- Desactivar el registro y volcar el archivo resultante a mi portátil con el puente de depuración:
adb pull /sdcard/btsnoop_hci.log ./captura_lapiz.log
Abrí captura_lapiz.log en Wireshark. Filtrando por protocolo RFCOMM y transferencias OBEX, apareció la secuencia completa: una negociación inicial de canal serie seguida de una ráfaga de paquetes que transportaban un archivo binario de casi 14 megabytes denominado internamente pack_story_03.dat.
Extraje el stream binario directo a disco mediante la opción Export Packet Bytes de Wireshark. Tenía el paquete en mis manos.
2. Radiografía del binario: Detectando patrones en el volcado hexadecimal
Al intentar abrir pack_story_03.dat con reproductores de medios o visores de archivos comprimidos habituales, el sistema devolvió un error de formato irreconocible. Momento de abrir la terminal y pasarle xxd:
xxd -g 1 pack_story_03.dat | head -n 30
Las primeras líneas arrojaron una estructura llamativa:
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..
Examinar el volcado arrojó varias pistas inmediatas:
- Cabecera reconocible: Los primeros 4 bytes corresponden a
OIDP(código ASCII para Optical Identification Package). - Metadatos iniciales: Los bytes
0x04a0x07marcan la versión de formato (0x00000001), seguidos de enteros little-endian de 32 bits que representan la cantidad de índices y el tamaño total del paquete. - Entropía sospechosa con cadencia fija: A partir del offset
0x0010, ciertas secuencias de 8 bytes se repetían exactamente igual en bloques adyacentes.
Cuando un archivo binario contiene regiones con repeticiones periódicas de secuencias idénticas en lugar de parecer ruido blanco puro, nos encontramos ante una ofuscación simple y no ante un cifrado de bloque con vector de inicialización como AES-CBC.
Los microcontroladores de bajo coste integrados en juguetes infantiles priorizan no agotar la batería ni saturar la CPU; no pueden permitirse ciclos pesados de descifrado en tiempo real mientras el niño desliza la punta sobre el papel.
3. Reventando la máscara: La ventaja de los ceros predecibles
En casi cualquier formato de archivo que contenga tablas de particiones o bloques de alineación, existen zonas de relleno compuestas íntegramente por bytes nulos (0x00).
La operación XOR cumple una propiedad aritmética fundamental:
$$X \oplus 0 = X$$
Si el empaquetador ofusca un bloque relleno de ceros aplicando un operador XOR con una clave cíclica, el resultado almacenado en el archivo no es otro que la propia clave repetida una y otra vez.
Observando las líneas 0x0010 y 0x0030, la secuencia a3 5f 12 7b c8 44 91 02 aparecía duplicada. Salté al final de la tabla de cabeceras, donde suele ubicarse el relleno de alineación a sectores de 512 bytes, y contrasté los bloques.
Efectivamente: la clave era una máscara cíclica de 16 bytes:
\xa3\x5f\x12\x7b\xc8\x44\x91\x02\xd4\x11\x8e\x33\xf9\xaa\x45\x60
Al aplicar esta máscara mediante un bucle XOR a partir del offset 0x0040, la niebla se disipó por completo.
Apareció una tabla limpia de punteros contiguos. Cada registro ocupaba exactamente 16 bytes:
- 2 bytes (uint16 little-endian): Código OID de la coordenada o página.
- 2 bytes (uint16 little-endian): Identificador de la pista o frase.
- 4 bytes (uint32 little-endian): Desplazamiento (offset) absoluto en el archivo.
- 4 bytes (uint32 little-endian): Longitud de la muestra de sonido en bytes.
- 4 bytes: Checksum CRC32 del segmento de audio.
Y lo mejor: al saltar al primer offset indicado por la tabla, encontramos la cabecera del primer segmento de audio desencriptado:
ff fb 90 64 00 00 00 00 ...
Ese 0xFF 0xFB es la firma inconfundible de sincronismo de una trama MPEG-1 Audio Layer 3 (MP3) a 128 kbps y 44.1 kHz.
4. El extractor: Automatizando el volcado con Python
Con la estructura completamente cartografiada, escribí un script en Python para procesar el binario, desarmar la máscara, leer la tabla de metadatos y volcar de forma ordenada tanto los audios MP3 individuales como un manifiesto JSON con la correspondencia entre los códigos OID del libro y cada archivo sonoro.
#!/usr/bin/env python3
"""
Extractor de recursos para paquetes de lápiz óptico interactivo.
Parsea el contenedor, remueve la máscara XOR y extrae pistas MP3.
"""
import json
import struct
import sys
from pathlib import Path
# Máscara cíclica identificada en el volcado
XOR_KEY = bytes([
0xA3, 0x5F, 0x12, 0x7B, 0xC8, 0x44, 0x91, 0x02,
0xD4, 0x11, 0x8E, 0x33, 0xF9, 0xAA, 0x45, 0x60
])
def desofuscar_bloque(buffer: bytes, key: bytes, offset_inicial: int = 0) -> bytes:
"""Aplica la máscara XOR cíclica teniendo en cuenta el desplazamiento."""
resultado = bytearray(len(buffer))
key_len = len(key)
for i, byte in enumerate(buffer):
key_byte = key[(offset_inicial + i) % key_len]
resultado[i] = byte ^ key_byte
return bytes(resultado)
def extraer_paquete(archivo_origen: Path, directorio_destino: Path) -> None:
directorio_destino.mkdir(parents=True, exist_ok=True)
with open(archivo_origen, "rb") as f:
datos = f.read()
# Validación de cabecera mágica OIDP
magic = datos[:4]
if magic != b"OIDP":
raise ValueError(f"Firma de cabecera inválida: {magic}")
version, total_entradas, offset_tabla = struct.unpack_from("<III", datos, 4)
print(f"Versión de contenedor: {version}")
print(f"Total de fragmentos indexados: {total_entradas}")
print(f"Offset tabla de asignación: 0x{offset_tabla:04X}")
manifiesto = []
cursor_tabla = offset_tabla
tamano_registro = 16
for i in range(total_entradas):
# Leemos y desofuscamos el registro de índice
chunk_indice = datos[cursor_tabla : cursor_tabla + tamano_registro]
indice_claro = desofuscar_bloque(chunk_indice, XOR_KEY, cursor_tabla)
oid_code, frase_id, audio_offset, audio_len = struct.unpack("<HHII", indice_claro[:12])
cursor_tabla += tamano_registro
# Extracción y descifrado del segmento de audio MP3
payload_cifrado = datos[audio_offset : audio_offset + audio_len]
audio_mp3 = desofuscar_bloque(payload_cifrado, XOR_KEY, audio_offset)
# Verificación rápida del frame sync MP3
if audio_mp3[:2] != b"\xff\xfb" and audio_mp3[:3] != b"ID3":
print(f"Aviso: Offset 0x{audio_offset:X} no muestra cabecera MP3 clásica.")
nombre_archivo = f"oid_{oid_code:05d}_pista_{frase_id:02d}.mp3"
ruta_salida = directorio_destino / nombre_archivo
with open(ruta_salida, "wb") as f_out:
f_out.write(audio_mp3)
manifiesto.append({
"oid_code": oid_code,
"frase_id": frase_id,
"archivo": nombre_archivo,
"tamano_bytes": audio_len
})
# Guardado de metadata para vincular con páginas físicas
with open(directorio_destino / "indice_libro.json", "w", encoding="utf-8") as f_json:
json.dump(manifiesto, f_json, indent=2)
print(f"Completado: {len(manifiesto)} pistas extraídas en {directorio_destino}/")
if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"Uso: python {sys.argv[0]} <archivo.dat> <carpeta_salida>")
sys.exit(1)
extraer_paquete(Path(sys.argv[1]), Path(sys.argv[2]))
Ejecuté el script contra pack_story_03.dat:
python extractor.py pack_story_03.dat ./audios_extraidos/
En cuestión de tres segundos, la carpeta se llenó con 84 archivos .mp3 perfectamente reproducibles en mplayer o cualquier reproductor de audio, acompañados de su índice JSON. Las locuciones de los actores de voz, las canciones y los diálogos estaban íntegros, con una claridad de audio impecable.
5. El valor de inspeccionar nuestros propios dispositivos
Conseguir los audios y el mapa de códigos OID en claro abre la puerta a usos mucho más interesantes que limitarse a depender del hardware original:
- Respaldos de audio accesibles: Si el lápiz sufre una caída fatal o se agota su batería de litio no sustituible, las grabaciones pedagógicas de los libros no quedan atrapadas para siempre.
- Catalogación digital: Cruzando el archivo
indice_libro.jsoncon transcripciones de texto, resulta trivial crear una interfaz web o una aplicación de escritorio para repasar el vocabulario de las lecciones sin necesidad de cargar físicamente con el lápiz. - Comprensión del hardware cotidiano: Casi todos estos dispositivos educativos comparten arquitecturas de chipsets económicos diseñados para reproducir muestras multimedia a partir de eventos ópticos puntuales, confiando su protección comercial a un simple velo de ofuscación por rotación de bytes.
Nada sustituye la satisfacción de dedicar una tarde de fin de semana a desmontar un formato opaco y convertir un flujo de bytes cerrado en información libre, documentada y legible.