---
title: "Turning an e-reader into your own system: reverse-engineering the Kobo Libra 2"
url: "https://quantarmy.com/en/reverse-engineering-kobo-libra-2-plato/"
description: "Hardware hacking a Kobo Libra 2: reverse-engineering its boot, a static C boot selector that verifies the system by sha256 on every boot, an own kernel, and Plato turned into the whole operating system. With the threat model and numbers measured on the device."
lang: "en"
---

# Turning an e-reader into your own system: reverse-engineering the Kobo Libra 2

A 512 MB e-reader with an i.MX6SLL SoC, no secure boot, and a user partition you can mount over USB. We reverse-engineered its boot, wrote an init that verifies the system by sha256 on every boot, and turned Plato into the whole OS. This is the process, the results, and what broke along the way.

Nov 4, 2026 · 20 min read · [Leer en español](https://quantarmy.com/ingenieria-inversa-kobo-libra-2-plato/)

- [sec](https://quantarmy.com/tag/sec/)

It is a book reader. An e-ink screen, Wi-Fi, 512 MB of RAM, a SoC NXP stopped calling new almost a decade ago. The kind of device nobody looks at with an attacker's eyes because "there's nothing inside." And yet it has everything that makes a hardware target interesting: a boot chain with **no secure boot**, an OTA update mechanism that applies packages on implicit trust, a user partition that mounts over USB like any thumb drive, and a counter in the MBR that decides whether the device wipes itself.

This article is the record of what we did to a **Kobo Libra 2**: reverse-engineering its boot and update mechanism, a static C boot selector that verifies the system by sha256 on every boot, an own kernel, and turning **Plato** —an open-source document reader— into the device's whole operating system. The public result is called **PlatoX** and is free software (AGPL-3.0); the personal version, with more things wired in, was called **KoboX**. We'll keep the two apart carefully, because security lives in the details and mixing editions is lying with precision.

This is not a jailbreak tutorial. It is a hardware-security case study: what you can learn from a closed device by reading its own firmware, where the real trust boundaries lie, and why "integrity" and "authenticity" are not the same word.

## The ecosystem you had to understand before touching anything

First rule of any intervention on someone else's hardware: reconstruct the stock system before changing a single byte. You cannot defend —or break with control— what you don't understand.

The device is a **Kobo Libra 2**, internal name `io`, board **E70K10**, confirmed by reading the PCB table in the firmware's own `kobo_config.sh`. Inside: an **NXP i.MX6SLL** SoC (ARM Cortex-A9), a Rohm **BD71828** PMIC, a **Realtek RTL8723DS** Wi-Fi part over SDIO, a **1264×1680** e-ink panel and **512 MB** of RAM. The stock software stack, rebuilt piece by piece from the extracted firmware, is a museum of 2015-era embedded Linux:

The stock stack: from U-Boot to Nickel, without a single signed link

- **Boot:** NXP's U-Boot (`2016.03-00128` on the 4.38 branch), **no signature verification**. Whoever controls what the kernel runs as init controls the device.
- **Kernel:** Linux **4.1.15**, Kobo's exact build `-00868-g58a2758be07`, compiled with GCC 5.3.0.
- **Userland:** `busybox 1.31.1` as PID 1, **glibc 2.19**, libstdc++ from GCC 4.9. udev 142, `wpa_supplicant 2.9` over **OpenSSL 1.0.1u**.
- **The reader:** **Nickel**, Kobo's Qt application, with its store and account.
- **`pickel`:** a shell wrapper that execs `pickel-imx` (an ELF). Keep it in mind: `pickel` is what decides whether the device factory-resets or accepts an update. Without knowing it, it is a security component.

Storage is an eMMC `/dev/mmcblk0` with three partitions and several raw zones outside them:

| Zone        | What it is                                      | Size / position |
| ----------- | ----------------------------------------------- | --------------- |
| `mmcblk0p1` | `rootfs` (where Nickel runs)                    | 256 MB ext4     |
| `mmcblk0p2` | `recoveryfs`                                    | 384 MB ext4     |
| `mmcblk0p3` | `KOBOeReader` (the "onboard", visible over USB) | 29.1 GB vfat    |
| zImage      | raw kernel                                      | sector 2048     |
| hwconfig    | hardware config                                 | sector 1024     |
| waveform    | e-ink table                                     | sector 14336    |

That `256 MB` rootfs and that `vfat` user partition are not trivia: both bit us later. The reference firmware was pinned to **4.38.23828** (the kernel hashes the system accepts are anchored to it).

## Reverse engineering: process and results

With the map in hand, the operational question is a single one: **how do you get your own code in without opening the device or touching U-Boot?** The answer was inside Kobo's own update mechanism.

### The vector: `KoboRoot.tgz` and implicit trust

Kobo updates like this: if the stock init finds a `.kobo/KoboRoot.tgz` file in storage, it unpacks it over `/` as root and reboots. In the stock `rcS` that happens around lines 251–269, with `pickel can-upgrade` as the gatekeeper. No signature. No origin check. It is a tar/gzip of root-owned files extracted over the system, and binaries must be mode **755**.

Put in threat language: **the updater is the vector**. Anyone who can write to the user partition —which mounts over USB with no credentials— can drop a `KoboRoot.tgz` that the system will apply as root on the next boot. The device's whole chain of trust rests on nobody dropping a file into a folder. That is the link that turns a closed reader into an open system, and the one a secure-boot design would close.

### The boot counter in the MBR

The prettiest finding of the whole reverse-engineering effort wasn't in a binary but in five bytes of the MBR. On `/dev/mmcblk0`, offsets **0x133–0x137**, Kobo keeps a **boot counter**. On each boot, `pickel should-factory-reset` increments it; when the index reaches **4**, the device shows the factory-reset screen and wipes itself. The only thing that resets that counter is **Nickel**, which writes the sequence `8d ef 02 d2 00` when it boots cleanly.

The security consequence runs deep: **a counter on disk is security state**, and if your system replaces Nickel, it inherits its obligations. An own system that boots instead of Nickel and doesn't reset that counter self-destructs in four reboots. We had to replicate Nickel's exact gesture —write `8d ef 02 d2 00` at offset `307`, re-read and log— only once Plato had been alive past a time threshold. A mutation test (writing at offset `308`) had to fail, and it did. That landed in version 1.0.3, after watching the device wipe itself on the bench.

### The bricks, told as what they are: results

The honest part of any hardware work is the list of times you left it unusable. Each brick was a finding about the system:

- **Permissions (`EACCES`):** we packaged `rcS` as mode `644`. `busybox init` invokes it with `execv`, which requires the execute bit → `EACCES` → nothing boots. Fixed by shipping it `755` with a `check_exec` guard. (fix 1.0.1)
- **Space (`ENOSPC`):** we put 44.5 MB into a 256 MB rootfs that was already at 93%. `etc/` extracted half-way and the device stalled at the splash, never reaching Plato. The fix was to move Plato to the **onboard** (vfat) and leave the rootfs with a **29 KB** delta, with a hard limit of ≤ 256 KB on the system partition. (fix 1.0.2)
- **The self-inflicted factory reset:** an early *debloat* phase put everything under `/usr/local/Kobo/*` on the delete list… including `pickel-imx`. Next boot: `pickel should-factory-reset` returns 127, the system reads that as "must reset", enters recovery, reflashes firmware **4.33.19747** and **wipes the onboard**. Out of that came the project's single most important rule: a **protect-list** that never touches `pickel*`, `udev`, `init.d`, the U-Boot images or `ld.so.preload`. Debloat, from then on, was done with **0-byte placeholders** inside the `KoboRoot.tgz`, never `rm` on the live system.

Expand

**When the device no longer boots: RAM recovery over fastboot.**

After one of those bricks we had to build a lifeline that didn't require opening the device. The Kobo, like many i.MX6 parts, exposes a **fastboot** mode that enumerates as USB `18d1:0d02` ("USB download gadget"), but its `boot` command calls NXP's `boota` routine and **expects an Android `boot.img` v0**, not a classic uImage. With `mkbootimg` (kernel at `0x80008000`, ramdisk at `0x83800000`, DTB as *second* at `0x83000000`) the device enumerates as "KoboRescue" in four seconds and boots a rescue system from RAM, without touching the eMMC.

The detail that turned the rescue tool into a dangerous one: disassembling U-Boot we found that `_detect_bootmode` runs inside `bootz`, and that holding *power* + *page* for a couple of seconds after entering fastboot triggers "recovery cus 9" → `root=/dev/mmcblk0p2` → the recovery `rcS` reflashes 4.33's U-Boot and kernel **and formats p1 and the onboard, without asking**. The MBR counter doesn't intervene here. The tool was fixed to claim the interface immediately and send `getvar:version`, disarming the one-second timeout that triggered that path.

### Kernel and modules: trust rebuilt from the binary

Wanting to load our own modules (zram, tun) into Kobo's kernel forces you to rebuild the trust Kobo doesn't publish. The kernel verifies `vermagic` and the per-symbol **CRCs**; a module built against headers that don't match to the bit is rejected with `Invalid argument`.

The process was binary archaeology:

- Extract the device's own `config.gz` (via `IKCONFIG_PROC`) and its `kallsyms` table: **75,337 symbols**.
- Rebuild the stock kernel's `Module.symvers` straight from the official zImage, with an **own LZO1X decompressor** to open it and locating `__ksymtab`/`__kcrctab` with kallsyms: **6,847 symbols**, all cross-checked against `kallsyms`.
- Verify each module with `check-module.py` (`vermagic` identity) and `verify-crcs.sh`, recomputing the CRCs from our own sources: **186 equal, 0 different**.

The Libra 2 (2021) kernel tree didn't carry this board's drivers, but the **Clara 2E** (2023) one matched the device config except for one option. The builds came out **reproducible**: `tun.ko` and `zsmalloc.ko` byte-for-byte identical across builds. That's the difference between "seems to work" and "I know exactly what I'm loading into ring 0."

## What Plato is, and what PlatoX is

**Plato** is an open-source document reader for e-readers, written in **Rust** by Bastien Dejean, meant as an alternative to Nickel: fast, no store, no account. It is the reader; it is not the system.

**PlatoX** is the idea that gives meaning to everything above: take Plato and turn it into the device's **whole operating system**. Instead of patching Nickel, we replace it entirely. The device boots a small **Buildroot system from RAM**, verified by sha256, and lands straight in Plato: no store, no account, no sync service. Technically, PlatoX is **Plato 0.9.45 plus a nine-patch series** (`plato-patches/0001–0009`) and a handful of own Rust *crates*. Nickel stays installed and untouched: if anything fails, the Kobo boots Nickel as always.

It's worth fixing the two names before we go on, because from here it matters:

- **KoboX** (and its evolution KoboX 2 / "0.2 Master Edition") is the **full personal firmware**, with things that depend on private infrastructure: Tailscale, a terminal, markets, weather, news, Home Assistant, library sync.
- **PlatoX 0.1** is the **public distillation** (AGPL-3.0): Plato as the system, the optimizations, the AI assistant and the calculator. Everything personal was removed on purpose, and with it much of the attack surface.

Three layers: verified boot, Plato as the UI, and the Rust crates behind it

## The new features on Plato

On top of Plato we added features that turn it into a complete environment. I group them by edition, because a security reader will want to know exactly what code runs in the public version.

### In public PlatoX (open source)

- **Plato as the launcher** (patch 0001): paths from the launcher, inputs by name. This is what lets Plato *be* the system rather than one more app.
- **Network panel and Wi-Fi picker** (patch 0002): a new `view/network_panel.rs` (+281 lines) that opens, from any view, a panel with Wi-Fi status in readable sentences (`Off / Looking for a known network… / Connected to X / No known network nearby`), IP and signal. The top-bar icon has four states (`wifi-off`, `wifi-connecting`, `wifi`, `wifi-secure`) and a *poller* thread updates it every 2 s, never from the UI thread.
- **AI assistant** (patches 0004 and 0009): on a text selection, **Summarize, Translate or Explain**; on a book, *insights* —a short review, an extended review, a summary or a navigable **concept map** (`view/concept_map.rs`, +801 lines). It works **with your own key**: OpenAI, **Claude** (via the native Messages API with SSE *streaming*, with `claude-opus-5-5` preselected when your key lists it), DeepSeek or any compatible server on your network. Answers are **cached offline**, and Wi-Fi only comes on when a request needs it, turning off after five idle minutes.
- **Phone-style calculator** (patch 0006): replaces Plato's old command-line calculator with a keypad one. Behind it is the `platox-calc` crate, with an **exact decimal** (`0.1 + 0.2 = 0.3`, not `0.30000000000000004`), percent and discount (`120 − 15 % = 102`, with a "Discount: 18" line), square root and guarded division by zero.
- **Clean menu, About and System Info** (patch 0005): the menu fits on one page, Reboot and Shutdown stay visible, and Wi-Fi management moves entirely into the network panel. Along the way, System Info **fixes a bug in Plato itself**, which showed the load average multiplied by 100 with a "%" sign, and free memory instead of available memory.
- **Developer tools** (patch 0007): *Test All* and *Benchmark*, behind a developer marker, with **secret redaction** in the diagnostics (no api-keys, tokens or PSKs in the logs).

### In KoboX (personal firmware, outside the public version)

This exists and works, but depends on private infrastructure and was removed from the public build to shrink the surface: **Tailscale** (first in userspace mode, then with TUN and an own module), a **terminal** (busybox shell over a pty, with `fastfetch` and `ssh`), **Markets** (Yahoo data), **Weather** (Open-Meteo), **News** (an own generator with `trafilatura` in an SSRF-hardened container), **Home Assistant** (lights and climate through a CONNECT tunnel) and **PDF sync** against an own Calibre-Web.

## The final optimization: faster, more robust, more responsive, more up to date

A reader that takes two seconds to change chapter is unusable, however elegant its architecture. The last phase was a performance campaign with one strict rule: **measure on the device, not on the host**. These are the medians, iteration by iteration, of version 1.4:

| Metric (median)              | Before (beta7) | After (beta12) |
| ---------------------------- | -------------- | -------------- |
| Page turn (EPUB)             | 231 ms         | **31 ms**      |
| Chapter change               | 2967 ms        | **46 ms**      |
| Open / reopen book           | 1265 ms        | **74 ms**      |
| First *paint* after power-on | 9.4 s          | **6.9 s**      |
| Wi-Fi connected              | 12–13 s        | **5.0 s**      |

Chapter change, from 2967 to 46 ms: where it dropped and why

### Faster

The work (patch 0003) attacks every layer of the cost:

- **Page turn:** a direct gray8-to-RGB565 copy with a 256-entry table (`framebuffer/fast16.rs`) and the *prerender* of the neighbour page on a separate thread (nice 5). beta8's page turn fell from 230.7 to 42.8 ms.
- **Text and layout:** per-word *shaping* cache, reused HarfBuzz buffers, hyphenation patterns loaded lazily per language, CSS parsed once per document.
- **Chapters without waits:** next-chapter layout in a *worker* and an **on-disk layout cache** (`prefetch.rs`, `layoutcache.rs`, `dlcodec.rs`), verified by dev/inode/size/mtime. That's the bar that collapses in the chart.
- **Compilation:** MuPDF, HarfBuzz, FreeType and DjVuLibre built from source with **LTO and NEON** (layout dropped from 13.4 to **9.8 ms/KB**), and the release profile with `lto` *fat*, `codegen-units = 1` and `target-cpu=cortex-a9`. The binary shrank from 10,494,280 to 8,851,200 bytes (−16%).
- **E-ink:** partial refresh between pages and a full refresh every 20 to control *ghosting*, plus `zram` to stretch the 512 MB.

### More responsive

Responsiveness isn't only the UI. Wi-Fi took 10 seconds to scan because of a fixed timeout in the `wext` driver; switching it to `nl80211,wext` dropped the scan from 10.5 to **1.4 s** and auto-connect to **5.0 s**. Shutdown and restart were measured at **0.18–0.23 s** between the request and the clean unmount of the onboard.

### More robust

Robustness, in this project, is a security property. The heart is `platox-boot`, a **static C** program installed as PID 1 on Kobo's system partition. On every boot it verifies the kernel, a `boot.manifest` and the **sha256** of the system image; and if anything fails —or if **three boots in a row never reach Plato**— it hands control to the stock init, Nickel. Plato's first *paint* resets that counter.

The boot selector: three checks and a safety net

The kernel-slot writer is just as careful: it writes with `dd conv=notrunc,fsync`, reads back and checks the sha256, and if it doesn't match it **restores the verified previous copy** without rebooting, all under `timeout 300` and only with the battery at 30% or charging. KoboX 1.4's own init goes further and **rejects foreign update packages**: it applies only its own, with a manifest (sha256, size, paths, space, battery), and sets any stray `KoboRoot.tgz` aside without running it. The stable version (Master Edition) rounded this off with atomic config writes and a clean `clippy -D warnings` across all eight crates.

### More up to date

Where the stock system was a 2015 museum, the own userland is today's: **Buildroot 2025.02 LTS**, glibc 2.41, busybox 1.37, `wpa_supplicant 2.12` with **mbedTLS** and **WPA3/SAE/OWE** support — and **no OpenSSL, no udev and no systemd**. There's even an own **4.1.15** kernel rebuilt from Kobo's GPL sources with reliable suspend: the device hung on suspend, and by **config bisection** we found the cause was, paradoxically, *removing* `CONFIG_PM_DEBUG`; those debug messages were hiding a *race* in the board's suspend, so the fix was to leave it as in stock.

## Method note and threat model

What this is and what it isn't, stated plainly:

- **There's integrity, not authenticity.** `platox-boot` verifies the system's sha256 on every boot, but **there are no signatures**. A local attacker who can write to the onboard partition can replace the `cpio` and its manifest at once. This design's threat model is **anti-brick and anti-corruption**, not anti-attacker with physical access and write permission. Closing that would require the one thing we didn't touch: U-Boot and a root of trust in the SoC.
- **The API key is stored in plain text**, in `.platox/config/ai.toml`, on the Kobo's own storage. It's documented that way and the recommendation is explicit: use a revocable key with a spending limit. The UI masks it to its last four characters and it never reaches the logs; the diagnostics redact secrets before writing anything.
- **It doesn't phone home.** No telemetry, no account, no update check. The device talks to the AI provider you configure, only when you ask it to, and to `pool.ntp.org` for the time. Wi-Fi is off unless an action needs it.
- **Debloat doesn't delete, it hides.** It's done with 0-byte placeholders inside the update package and a protect-list that never touches the critical pieces. It is reversible and cannot, by construction, repeat the brick that inspired it.
- **Supply chain:** sources pinned by version and sha256, reproducible kernel and packages, and a `check-public.sh` that refuses personal data, private infrastructure and Kobo's proprietary files before every release.

## What we take away

- **The updater is the vector.** An OTA mechanism that applies packages on implicit trust —no signature, over a partition that mounts via USB— is the front door, not a side detail. If you design hardware, that's where the signature goes.
- **State on disk is security state.** Five bytes in the MBR decided whether the device wiped itself. Whoever replaces the system inherits its invariants; discovering them late costs a device.
- **Integrity isn't authenticity.** A sha256 per boot protects you from corruption and bricking, not from an attacker who controls the bit. Knowing which of the two problems you're solving is half the work.
- **Bricks are data.** Every device we left unusable —permissions, space, one too many `pickel-imx` on a delete list— taught a system invariant no documentation told us.

A 512 MB reader turned out to be a full hardware-security target: an unsigned boot chain, an implicit-trust updater, security state on disk and a recovery mode with sharp teeth. Turning it into a verified, fast system of our own was the excuse; the trust map that came out of the process is what we take away.

## Resources

- **Plato**, by Bastien Dejean — the open-source reader at the heart of it all: [github.com/baskerville/plato](https://github.com/baskerville/plato).
- **PlatoX** — the public edition (AGPL-3.0) of this work; no public repository yet.
- **Buildroot** — the embedded system that boots in RAM: [buildroot.org](https://buildroot.org/).

// Versión en español · [Convertir un e-reader en tu sistema: ingeniería inversa del Kobo Libra 2](https://quantarmy.com/ingenieria-inversa-kobo-libra-2-plato/)

## More in sec

Nov 4, 2026 · 16 min

### [Whose router is it? Reverse-engineering a Digi Comtrend GRG-4355u](https://quantarmy.com/en/reverse-engineering-the-digi-comtrend-grg-4355u-router/)

The router your ISP hands you is a small OpenWrt you don't fully control: a low-privilege account that can rewrite its own permissions, a root hash in DES, and a TR-069 agent your operator uses to reconfigure the box remotely. This is the reverse engineering, what we found, and how we took back control — no secrets, no recipes.

[sec](https://quantarmy.com/tag/sec/)
