Convertir un e-reader en tu sistema: ingeniería inversa del Kobo Libra 2
Un lector de libros de 512 MB con un SoC i.MX6SLL, sin secure boot y con una partición de usuario montable por USB. Le hicimos ingeniería inversa al arranque, escribimos un init propio que verifica el sistema por sha256 en cada boot y convertimos Plato en el sistema operativo entero. Esto es el proceso, los resultados y lo que se rompió por el camino.
// Sec · QUANTARMY research
Convertir un e-reader en tu sistema
Ingeniería inversa del Kobo Libra 2 y PlatoX
Es un lector de libros. Pantalla e-ink, Wi-Fi, 512 MB de RAM, un SoC que NXP dejó de considerar nuevo hace casi una década. La clase de aparato que nadie mira con ojos de atacante porque "no tiene nada dentro". Y sin embargo tiene todo lo que hace interesante a un objetivo de hardware: una cadena de arranque sin secure boot, un mecanismo de actualización OTA que aplica paquetes con confianza implícita, una partición de usuario que se monta por USB como una memoria cualquiera, y un contador en el MBR que decide si el aparato se borra solo.
Este artículo es el registro de lo que le hicimos a un Kobo Libra 2: ingeniería inversa de su arranque y su mecanismo de actualización, un selector de arranque propio en C que verifica el sistema por sha256 en cada boot, un kernel propio, y la conversión de Plato —un lector de documentos open-source— en el sistema operativo entero del dispositivo. El resultado público se llama PlatoX y es software libre (AGPL-3.0); la versión personal, con más cosas enganchadas, se llamó KoboX. Vamos a separar las dos con cuidado, porque la seguridad está en los detalles y mezclar ediciones es mentir con precisión.
No es un tutorial de jailbreak. Es un caso de estudio de seguridad del hardware: qué se puede aprender de un dispositivo cerrado leyendo su propio firmware, dónde están los límites reales de confianza, y por qué "integridad" y "autenticidad" no son la misma palabra.
El ecosistema que había que entender antes de tocar nada
La primera regla de cualquier intervención sobre hardware ajeno: reconstruir el sistema de serie antes de cambiar un solo byte. No se puede defender —ni romper con control— lo que no se entiende.
El aparato es un Kobo Libra 2, nombre interno io, placa E70K10, confirmado leyendo la tabla de PCB de kobo_config.sh del propio firmware. Dentro: un SoC NXP i.MX6SLL (ARM Cortex-A9), PMIC Rohm BD71828, Wi-Fi Realtek RTL8723DS por SDIO, pantalla e-ink de 1264×1680 y 512 MB de RAM. El stack de software de serie, reconstruido pieza a pieza desde el firmware extraído, es un museo de Linux embebido de 2015:
- Arranque: U-Boot de NXP (
2016.03-00128en la rama 4.38), sin verificación de firma. Quien controle lo que el kernel ejecuta como init, controla el aparato. - Kernel: Linux 4.1.15, build exacto de Kobo
-00868-g58a2758be07, compilado con GCC 5.3.0. - Userland:
busybox 1.31.1como PID 1, glibc 2.19, libstdc++ de GCC 4.9. udev 142,wpa_supplicant 2.9sobre OpenSSL 1.0.1u. - El lector: Nickel, la aplicación Qt de Kobo, con su tienda y su cuenta.
pickel: un wrapper de shell que ejecutapickel-imx(un ELF). Guárdalo en la cabeza:pickeles quien decide si el aparato hace factory-reset o acepta una actualización. Es, sin saberlo, una pieza de seguridad.
El almacenamiento es un eMMC /dev/mmcblk0 con tres particiones y varias zonas crudas fuera de ellas:
| Zona | Qué es | Tamaño / posición |
|---|---|---|
mmcblk0p1 | rootfs (donde corre Nickel) | 256 MB ext4 |
mmcblk0p2 | recoveryfs | 384 MB ext4 |
mmcblk0p3 | KOBOeReader (el "onboard", visible por USB) | 29,1 GB vfat |
| zImage | kernel crudo | sector 2048 |
| hwconfig | config de hardware | sector 1024 |
| waveform | tabla e-ink | sector 14336 |
Ese 256 MB de la rootfs y ese vfat de la partición de usuario no son detalles: los dos nos mordieron más adelante. El firmware de referencia quedó fijado a la versión 4.38.23828 (los hashes de kernel que el sistema acepta están anclados a ella).
Ingeniería inversa: proceso y resultados
Con el mapa en la mano, la pregunta operativa es una sola: ¿cómo se mete código propio sin abrir el aparato ni tocar U-Boot? La respuesta estaba en el propio mecanismo de actualización de Kobo.
El vector: KoboRoot.tgz y la confianza implícita
Kobo actualiza así: si el init de serie encuentra un fichero .kobo/KoboRoot.tgz en el almacenamiento, lo desempaqueta sobre / como root y reinicia. En el rcS de serie eso ocurre alrededor de las líneas 251–269, con pickel can-upgrade como guardián. No hay firma. No hay verificación de origen. Es un tar/gzip de ficheros propiedad de root que se extrae sobre el sistema, y los binarios tienen que ir en modo 755.
Dicho en lenguaje de amenazas: el actualizador es el vector. Cualquiera que pueda escribir en la partición de usuario —que se monta por USB sin credenciales— puede colocar un KoboRoot.tgz que el sistema aplicará como root en el siguiente arranque. Toda la cadena de confianza del dispositivo se apoya en que nadie meta un fichero en una carpeta. Ese es el eslabón que convierte un lector cerrado en un sistema abierto, y el que un diseño con secure boot cerraría.
El contador de arranque en el MBR
El hallazgo más bonito de toda la ingeniería inversa no estaba en un binario, sino en cinco bytes del MBR. En /dev/mmcblk0, offsets 0x133–0x137, Kobo guarda un contador de arranque. En cada boot, pickel should-factory-reset lo incrementa; cuando el índice llega a 4, el aparato muestra la pantalla de factory-reset y se borra. Lo único que resetea ese contador es Nickel, que escribe la secuencia 8d ef 02 d2 00 cuando arranca bien.
La consecuencia de seguridad es profunda: un contador en disco es estado de seguridad, y si tu sistema sustituye a Nickel, hereda su obligación. Un sistema propio que arranque en lugar de Nickel y no resetee ese contador se autodestruye en cuatro reinicios. Tuvimos que replicar el gesto exacto de Nickel —escribir 8d ef 02 d2 00 en el offset 307, releer y registrar— solo cuando Plato llevaba vivo más de un umbral de tiempo. Un test de mutación (escribir en el offset 308) debía fallar, y fallaba. Eso llegó en la versión 1.0.3, después de verlo borrarse en el banco de pruebas.
Los bricks, contados como lo que son: resultados
La parte honesta de cualquier trabajo sobre hardware es la lista de veces que lo dejaste inservible. Cada brick fue un hallazgo sobre el sistema:
- Permisos (
EACCES): empaquetamosrcSen modo644.busybox initlo invoca conexecv, que exige el bit de ejecución →EACCES→ no arranca nada. Se corrigió enviándolo en755con uncheck_execde guarda. (fix 1.0.1) - Espacio (
ENOSPC): metimos 44,5 MB en una rootfs de 256 MB que ya iba al 93%. Eletc/se extendió a medias y el aparato se quedó en el splash sin llegar a Plato. La solución fue mover Plato al onboard (vfat) y dejar la rootfs con un delta de 29 KB, con un límite duro de ≤ 256 KB en la partición de sistema. (fix 1.0.2) - El factory-reset autoinfligido: una fase de debloat temprana metió todo lo de
/usr/local/Kobo/*en la lista de borrado… incluidopickel-imx. Siguiente arranque:pickel should-factory-resetdevuelve 127, el sistema interpreta eso como "hay que resetear", entra en recovery, reflashea el firmware 4.33.19747 y borra el onboard. De ahí nació la regla más importante del proyecto: una protect-list que nunca deja tocarpickel*,udev,init.d, las imágenes de U-Boot nild.so.preload. El debloat, además, pasó a hacerse con placeholders de 0 bytes dentro delKoboRoot.tgz, nunca conrmsobre el sistema vivo.
Kernel y módulos: confianza reconstruida desde el binario
Querer cargar módulos propios (zram, tun) en el kernel de Kobo obliga a reconstruir la confianza que Kobo no publica. El kernel verifica vermagic y los CRC de cada símbolo; un módulo compilado contra cabeceras que no cuadran al bit se rechaza con Invalid argument.
El proceso fue arqueología de binarios:
- Extraer del aparato su propio
config.gz(víaIKCONFIG_PROC) y su tablakallsyms: 75.337 símbolos. - Reconstruir el
Module.symversdel kernel de serie directamente desde el zImage oficial, con un descompresor LZO1X propio para abrirlo y localizando__ksymtab/__kcrctabcon kallsyms: 6.847 símbolos, todos contrastados contrakallsyms. - Verificar cada módulo con
check-module.py(identidad devermagic) yverify-crcs.sh, recomputando los CRC desde fuentes propias: 186 iguales, 0 distintos.
El árbol de kernel de la Libra 2 (2021) no traía los drivers de esta placa, pero el de la Clara 2E (2023) encajaba con la config del aparato salvo una opción. Las compilaciones salieron reproducibles: tun.ko y zsmalloc.ko idénticos byte a byte entre builds. Esa es la diferencia entre "parece que funciona" y "sé exactamente qué estoy cargando en ring 0".
Qué es Plato, y qué es PlatoX
Plato es un lector de documentos open-source para e-readers, escrito en Rust por Bastien Dejean, pensado como alternativa a Nickel: rápido, sin tienda, sin cuenta. Es el lector; no es el sistema.
PlatoX es la idea que da sentido a todo lo anterior: coger Plato y convertirlo en el sistema operativo entero del Kobo. En vez de parchear Nickel, lo sustituimos del todo. El aparato arranca un pequeño sistema Buildroot desde RAM, verificado por sha256, y cae directamente en Plato: sin tienda, sin cuenta, sin servicio de sincronización. Técnicamente, PlatoX es Plato 0.9.45 más una serie de nueve parches (plato-patches/0001–0009) y un puñado de crates de Rust propios. Nickel se queda instalado e intacto: si algo falla, el Kobo arranca Nickel como siempre.
Conviene fijar los dos nombres antes de seguir, porque de aquí en adelante importa:
- KoboX (y su evolución KoboX 2 / "0.2 Master Edition") es el firmware personal completo, con cosas que dependen de infraestructura propia: Tailscale, una terminal, mercados, tiempo, noticias, Home Assistant, sincronización de biblioteca.
- PlatoX 0.1 es la destilación pública (AGPL-3.0): Plato como sistema, las optimizaciones, el asistente de IA y la calculadora. Todo lo personal se quitó a propósito, y con ello buena parte de la superficie de ataque.
Las funcionalidades nuevas de Plato
Sobre Plato se añadieron funciones que lo convierten en un entorno completo. Las clasifico por edición, porque un lector de seguridad querrá saber exactamente qué código corre en la versión pública.
En PlatoX público (open source)
- Plato como launcher (parche 0001): rutas desde el arrancador, entradas por nombre. Es lo que permite que Plato sea el sistema y no una app más.
- Panel de red y selector de Wi-Fi (parche 0002): un
view/network_panel.rsnuevo (+281 líneas) que abre, desde cualquier vista, un panel con el estado del Wi-Fi en frases legibles (Apagado / Buscando una red conocida… / Conectado a X / No hay ninguna red conocida cerca), IP y señal. El icono de la barra tiene cuatro estados (wifi-off,wifi-connecting,wifi,wifi-secure) y un hilo poller lo actualiza cada 2 s, nunca desde el hilo de la UI. - Asistente de IA (parches 0004 y 0009): sobre una selección de texto, Resumir, Traducir o Explicar; sobre un libro, insights —reseña corta, reseña extendida, resumen o un mapa de conceptos navegable (
view/concept_map.rs, +801 líneas)—. Funciona con tu propia clave: OpenAI, Claude (vía la Messages API nativa con streaming por SSE, conclaude-opus-5-5preseleccionado si tu clave lo lista), DeepSeek o cualquier servidor compatible en tu red. Las respuestas se cachean offline y el Wi-Fi solo se enciende cuando una petición lo necesita, apagándose tras cinco minutos de inactividad. - Calculadora estilo teléfono (parche 0006): reemplaza la vieja calculadora de línea de comandos de Plato por una de teclas. Detrás va el crate
platox-calc, con un decimal exacto (0,1 + 0,2 = 0,3, no0,30000000000000004), porcentaje y descuento (120 − 15 % = 102, con la línea "Descuento: 18"), raíz cuadrada y división por cero controlada. - Menú limpio, About y System Info (parche 0005): el menú cabe en una página, Reiniciar y Apagar quedan siempre visibles, y la gestión de Wi-Fi se va entera al panel de red. De paso, System Info arregla un bug del propio Plato, que mostraba la carga del sistema multiplicada por 100 con un signo de "%", y la memoria libre en vez de la disponible.
- Herramientas de desarrollo (parche 0007): Test All y Benchmark, detrás de un marcador de desarrollador, con redacción de secretos en los diagnósticos (nada de api-keys, tokens o PSK en los logs).
En KoboX (firmware personal, fuera de la versión pública)
Esto existe y funciona, pero depende de infraestructura propia y se quitó del público para reducir superficie: Tailscale (primero en modo usuario, luego con TUN y módulo propio), una terminal (shell de busybox sobre un pty, con fastfetch y ssh), Markets (datos de Yahoo), Weather (Open-Meteo), News (un generador propio con trafilatura en un contenedor endurecido contra SSRF), Home Assistant (luz y clima por un túnel CONNECT) y sincronización de PDF contra un Calibre-Web propio.
La optimización final: más rápido, más robusto, más responsive, más actualizado
Un lector que tarda dos segundos en cambiar de capítulo es inusable, por muy elegante que sea su arquitectura. La última fase fue una campaña de rendimiento con una regla estricta: medir en el aparato, no en el host. Estas son las medianas, iteración a iteración, de la versión 1.4:
| Métrica (mediana) | Antes (beta7) | Después (beta12) |
|---|---|---|
| Pasar página (EPUB) | 231 ms | 31 ms |
| Cambio de capítulo | 2967 ms | 46 ms |
| Abrir / reabrir libro | 1265 ms | 74 ms |
| Primer paint tras encender | 9,4 s | 6,9 s |
| Wi-Fi conectado | 12–13 s | 5,0 s |
Más rápido
El trabajo (parche 0003) ataca cada capa del coste:
- Pasar página: una copia directa de gray8 a RGB565 con una tabla de 256 entradas (
framebuffer/fast16.rs) y el prerender de la página vecina en un hilo aparte (nice 5). El page turn de beta8 cayó de 230,7 a 42,8 ms. - Texto y layout: caché de shaping por palabra, buffers de HarfBuzz reutilizados, patrones de guionado cargados en diferido por idioma, CSS parseado una sola vez por documento.
- Capítulos sin esperas: layout del siguiente capítulo en un worker y caché de layout en disco (
prefetch.rs,layoutcache.rs,dlcodec.rs), con verificación por dev/inode/tamaño/mtime. Esa es la barra que se desploma en el gráfico. - Compilación: MuPDF, HarfBuzz, FreeType y DjVuLibre compilados desde fuente con LTO y NEON (el layout bajó de 13,4 a 9,8 ms/KB), y el perfil de release con
ltofat ycodegen-units = 1ytarget-cpu=cortex-a9. El binario encogió de 10.494.280 a 8.851.200 bytes (−16 %). - E-ink: refresco parcial entre páginas y refresco completo cada 20 para controlar el ghosting, más
zrampara estirar los 512 MB.
Más responsive
La responsividad no es solo la UI. El Wi-Fi tardaba 10 segundos en escanear por un timeout fijo del driver wext; cambiándolo a nl80211,wext, el escaneo bajó de 10,5 a 1,4 s y la autoconexión a 5,0 s. El apagado y reinicio se midió en 0,18–0,23 s entre la petición y el desmontaje limpio del onboard.
Más robusto
La robustez es, en este proyecto, una propiedad de seguridad. El corazón es platox-boot, un programa en C estático instalado como PID 1 en la partición de sistema de Kobo. En cada arranque verifica el kernel, un boot.manifest y el sha256 de la imagen del sistema; y si algo falla —o si se encadenan tres arranques que no llegan a Plato— cede el control al init de serie, Nickel. El primer paint de Plato resetea ese contador.
El escritor de la ranura del kernel es igual de cuidadoso: escribe con dd conv=notrunc,fsync, vuelve a leer y comprueba el sha256, y si no cuadra restaura la copia verificada previa sin reiniciar, todo bajo timeout 300 y solo con la batería al 30 % o cargando. El init propio de KoboX 1.4, además, rechaza paquetes de actualización ajenos: solo aplica los suyos, con manifiesto (sha256, tamaño, rutas, espacio, batería), y aparta cualquier KoboRoot.tgz extraño sin ejecutarlo. La versión estable (Master Edition) remató esto con escrituras atómicas de configuración y clippy -D warnings limpio en los ocho crates.
Más actualizado
Donde el sistema de serie era un museo de 2015, el userland propio es de hoy: Buildroot 2025.02 LTS, glibc 2.41, busybox 1.37, wpa_supplicant 2.12 con mbedTLS y soporte de WPA3/SAE/OWE — y sin OpenSSL, sin udev y sin systemd. Hay incluso un kernel propio 4.1.15 recompilado desde las fuentes GPL de Kobo con suspensión fiable: el aparato se colgaba al suspender, y por bisección de configuración encontramos que la causa era, paradójicamente, quitar CONFIG_PM_DEBUG; esos mensajes de depuración ocultaban una race en la suspensión de la placa, así que la solución fue dejarlo como en stock.
Nota de método y modelo de amenazas
Lo que esto es y lo que no es, dicho sin adornos:
- Hay integridad, no autenticidad.
platox-bootverifica el sha256 del sistema en cada arranque, pero no hay firmas. Un atacante local que pueda escribir en la partición onboard puede sustituir a la vez elcpioy su manifiesto. El modelo de amenazas de este diseño es anti-brick y anti-corrupción, no anti-atacante con acceso físico y permiso de escritura. Cerrar eso exigiría lo único que no tocamos: U-Boot y una raíz de confianza en el SoC. - La clave de API va en claro, en
.platox/config/ai.toml, en el almacenamiento del propio Kobo. Está documentado así y la recomendación es explícita: usa una clave revocable y con límite de gasto. La UI la enmascara a sus últimos cuatro caracteres y nunca llega a los logs; el diagnóstico redacta secretos antes de escribir nada. - No llama a casa. No hay telemetría, ni cuenta, ni comprobación de actualizaciones. El aparato habla con el proveedor de IA que tú configures, solo cuando se lo pides, y con
pool.ntp.orgpara la hora. El Wi-Fi está apagado salvo que una acción lo necesite. - El debloat no borra, oculta. Se hace con placeholders de 0 bytes dentro del paquete de actualización y una protect-list que jamás toca las piezas críticas. Es reversible y no puede, por construcción, repetir el brick que lo inspiró.
- Cadena de suministro: fuentes fijadas por versión y sha256, kernel y paquetes reproducibles, y un
check-public.shque rechaza datos personales, infraestructura privada y ficheros propietarios de Kobo antes de cada publicación.
Lo que nos llevamos
- El actualizador es el vector. Un mecanismo OTA que aplica paquetes con confianza implícita —sin firma, sobre una partición que se monta por USB— es la puerta de entrada, no una pieza secundaria. Si diseñas hardware, ahí es donde va la firma.
- El estado en disco es estado de seguridad. Cinco bytes en el MBR decidían si el aparato se borraba. Quien sustituye al sistema, hereda sus invariantes; descubrirlas tarde cuesta un dispositivo.
- Integridad no es autenticidad. Un sha256 por boot te protege de la corrupción y del brick, no de un atacante que controle el bit. Saber cuál de los dos problemas estás resolviendo es la mitad del trabajo.
- Los bricks son datos. Cada aparato que dejamos inservible —permisos, espacio, un
pickel-imxde más en una lista de borrado— enseñó una invariante del sistema que ninguna documentación contaba.
Un lector de 512 MB resultó ser un objetivo de hardware security completo: cadena de arranque sin firmar, actualizador de confianza implícita, estado de seguridad en disco y un modo de recuperación con los dientes afilados. Convertirlo en un sistema propio, verificado y rápido fue la excusa; el mapa de confianza que salió del proceso es lo que nos llevamos.
Recursos
- Plato, de Bastien Dejean — el lector open-source en el corazón de todo: github.com/baskerville/plato.
- PlatoX — la edición pública (AGPL-3.0) de este trabajo; sin repositorio público por ahora.
- Buildroot — el sistema embebido que arranca en RAM: buildroot.org.