<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>sec - QUANTARMY</title><description>Ciberseguridad ofensiva, defensiva y hardening.</description><link>https://quantarmy.com/</link><language>en</language><atom:link href="https://quantarmy.com/tag/sec/rss.xml" rel="self" type="application/rss+xml"/><item><title>Whose router is it? Reverse-engineering a Digi Comtrend GRG-4355u</title><link>https://quantarmy.com/en/reverse-engineering-the-digi-comtrend-grg-4355u-router/</link><guid isPermaLink="true">https://quantarmy.com/en/reverse-engineering-the-digi-comtrend-grg-4355u-router/</guid><description>The router your ISP hands you is a small OpenWrt you don&apos;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.</description><pubDate>Wed, 04 Nov 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The router your operator hands you isn&apos;t entirely yours. You pay for it in the monthly bill, it lives in your home and carries your traffic, but its firmware is the ISP&apos;s call, there are accounts you can&apos;t see, and through a remote-management channel the operator can reconfigure, update or re-provision it whenever it likes. For a security reader that&apos;s a threat-model question: &lt;strong&gt;who really controls this device, and what can an attacker with the sticker credentials do?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This article answers that for a concrete case: the &lt;strong&gt;Comtrend GRG-4355u&lt;/strong&gt; that &lt;strong&gt;Digi&lt;/strong&gt; ships for fiber. We reverse-engineered it —without opening the case, no UART, no JTAG, over the local network only—, found a chain of weaknesses leading from a guest account to root, and then turned it into a neutral, hardened router that no longer answers to the operator. All on our own equipment and line.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Responsible-research note.&lt;/strong&gt; Everything below is on an owned device, on an owned line, on the LAN, with reversible changes and documented rollback. We publish no credential, no hash, no serial number, and no step-by-step recipe: this is an analysis of &lt;strong&gt;vulnerability classes and their significance&lt;/strong&gt;, not a weapon against an ISP&apos;s fleet. One detail worth telling: the AI guardrail we worked with refused to perform the interactive root login itself, so a human ran those steps. Security is also done that way.&lt;/p&gt;
&lt;h2 id=&quot;the-ecosystem-a-vendor-openwrt-with-shared-owners&quot;&gt;The ecosystem: a vendor OpenWrt with shared owners&lt;/h2&gt;
&lt;p&gt;Before touching anything, the inventory. The GRG-4355u is made by &lt;strong&gt;Comtrend&lt;/strong&gt; (even though Digi distributes it), runs an &lt;strong&gt;Airoha EN7581&lt;/strong&gt; SoC (four ARM Cortex-A53 cores), and inside runs a &lt;strong&gt;vendor OpenWrt&lt;/strong&gt; with the &lt;strong&gt;LuCI&lt;/strong&gt; interface, kernel &lt;strong&gt;5.4.55&lt;/strong&gt;, under the firmware label &lt;strong&gt;DIG-1.3.22&lt;/strong&gt;. The units we&apos;ve seen report hardware 1.1 or 1.2.&lt;/p&gt;
&lt;p&gt;The interesting part isn&apos;t the hardware photo but the map of who&apos;s in charge of it:&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;10 2 1026.3 371&quot; width=&quot;1026&quot; height=&quot;371&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-router-ecosistema-en-t fig-router-ecosistema-en-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 1026px&quot;&gt;&lt;title id=&quot;fig-router-ecosistema-en-t&quot;&gt;Management planes of the Comtrend GRG-4355u: the EN7581 SoC runs firmware DIG-1.3.22 (vendor OpenWrt with LuCI); on the LAN live the LuCI web over HTTPS with rpcd/ubus, SSH that lands in a vendor CLI, and cxt_acl_lan, a vendor firewall outside UCI; above them, the ISP&apos;s ACS manages the device over TR-069 on the management VLAN&lt;/title&gt;&lt;desc id=&quot;fig-router-ecosistema-en-d&quot;&gt;Comtrend GRG-4355u (fiber HGU) Airoha EN7581 SoC · quad Cortex-A53 → Firmware DIG-1.3.22 vendor OpenWrt + LuCI · kernel 5.4.55; Firmware DIG-1.3.22 vendor OpenWrt + LuCI · kernel 5.4.55 → LuCI / HTTPS rpcd · ubus (JSON-RPC); Firmware DIG-1.3.22 vendor OpenWrt + LuCI · kernel 5.4.55 → dropbear SSH the account lands in /sbin/cli, not a shell; Firmware DIG-1.3.22 vendor OpenWrt + LuCI · kernel 5.4.55 → cxt_acl_lan vendor firewall, outside UCI; TR-069 / CWMP the ISP&apos;s ACS manages the device → Firmware DIG-1.3.22 vendor OpenWrt + LuCI · kernel 5.4.55 (mgmt VLAN · config, firmware, credentials)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-router-ecosistema-en-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-router-ecosistema-en-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;20&quot; y=&quot;265&quot; width=&quot;1006.3&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M725.3,75 L725.3,112 Q725.3,120 717.8,122.8 L624.6,158&quot; marker-end=&quot;url(#fig-router-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M728.5,214 L892.3,239.8 Q900.2,241 900.2,249 L900.2,260 Q900.2,268 900.2,276 L900.2,295&quot; marker-end=&quot;url(#fig-router-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M550.4,214 L550.4,233 Q550.4,241 550.4,249 L550.4,260 Q550.4,268 550.4,276 L550.4,295&quot; marker-end=&quot;url(#fig-router-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M368.1,212.6 L181.3,239.8 Q173.4,241 173.4,249 L173.4,260 Q173.4,268 173.4,276 L173.4,295&quot; marker-end=&quot;url(#fig-router-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M366.5,82 L366.5,112 Q366.5,120 374,122.7 L472.4,158&quot; marker-end=&quot;url(#fig-router-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;174.9&quot; y=&quot;109&quot; width=&quot;383.1&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;366.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;366.5&quot; y=&quot;120&quot;&gt;mgmt VLAN · config, firmware, credentials&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;26&quot; y=&quot;271&quot; width=&quot;118.3&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;31&quot; y=&quot;279&quot; dominant-baseline=&quot;central&quot;&gt;LAN management&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;552.1&quot; y=&quot;19&quot; width=&quot;346.5&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;725.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;725.3&quot; y=&quot;38&quot;&gt;Comtrend GRG-4355u (fiber HGU)&lt;/tspan&gt;&lt;tspan x=&quot;725.3&quot; y=&quot;56&quot;&gt;Airoha EN7581 SoC · quad Cortex-A53&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;368.1&quot; y=&quot;158&quot; width=&quot;364.7&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;550.4&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;550.4&quot; y=&quot;177&quot;&gt;Firmware DIG-1.3.22&lt;/tspan&gt;&lt;tspan x=&quot;550.4&quot; y=&quot;195&quot;&gt;vendor OpenWrt + LuCI · kernel 5.4.55&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;786.1&quot; y=&quot;295&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;900.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;900.2&quot; y=&quot;314&quot;&gt;LuCI / HTTPS&lt;/tspan&gt;&lt;tspan x=&quot;900.2&quot; y=&quot;332&quot;&gt;rpcd · ubus (JSON-RPC)&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;340.8&quot; y=&quot;295&quot; width=&quot;419.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;550.4&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;550.4&quot; y=&quot;314&quot;&gt;dropbear SSH&lt;/tspan&gt;&lt;tspan x=&quot;550.4&quot; y=&quot;332&quot;&gt;the account lands in /sbin/cli, not a shell&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;32&quot; y=&quot;295&quot; width=&quot;282.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;173.4&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;173.4&quot; y=&quot;314&quot;&gt;cxt_acl_lan&lt;/tspan&gt;&lt;tspan x=&quot;173.4&quot; y=&quot;332&quot;&gt;vendor firewall, outside UCI&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;g&gt;&lt;path class=&quot;fig-node&quot; d=&quot;M206.9,18 A159.6,6 0 0 1 526.1,18 V76 A159.6,6 0 0 1 206.9,76 Z&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-node-line&quot; d=&quot;M206.9,18 A159.6,6 0 0 0 526.1,18&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;text class=&quot;fig-text&quot; x=&quot;366.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;366.5&quot; y=&quot;42&quot;&gt;TR-069 / CWMP&lt;/tspan&gt;&lt;tspan x=&quot;366.5&quot; y=&quot;60&quot;&gt;the ISP&apos;s ACS manages the device&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;Who controls what: three LAN management planes and one remote ISP one&lt;/figcaption&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accounts:&lt;/strong&gt; there&apos;s an &lt;code&gt;admin&lt;/code&gt;, a low-privilege &lt;code&gt;user&lt;/code&gt; —whose credentials usually come on the sticker— and a &lt;code&gt;root&lt;/code&gt;. The catch: logging in over SSH with the normal accounts doesn&apos;t give a Linux shell but a &lt;strong&gt;restricted vendor CLI&lt;/strong&gt; (&lt;code&gt;/sbin/cli&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The web plane:&lt;/strong&gt; LuCI talks over HTTPS to &lt;code&gt;rpcd&lt;/code&gt;/&lt;code&gt;ubus&lt;/code&gt;, OpenWrt&apos;s call bus. That&apos;s where configuration decides what each login may read and write.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hidden control layer:&lt;/strong&gt; besides the standard firewall (UCI), the vendor installs &lt;code&gt;cxt_acl_lan&lt;/code&gt;, its own access control &lt;strong&gt;outside&lt;/strong&gt; UCI. That is, there are rules deciding which ports are open that you &lt;strong&gt;can&apos;t see&lt;/strong&gt; in the normal firewall config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The ISP, above everything:&lt;/strong&gt; a &lt;strong&gt;TR-069 / CWMP&lt;/strong&gt; agent connects to the operator&apos;s management server (ACS) over a separate management VLAN. That channel can push config, firmware and credentials. The homeowner can&apos;t turn it off from the interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last piece is the article&apos;s thesis: the device&apos;s strongest trust link isn&apos;t local, it&apos;s the operator&apos;s.&lt;/p&gt;
&lt;h2 id=&quot;reverse-engineering-from-guest-to-root-over-the-network&quot;&gt;Reverse engineering: from guest to root, over the network&lt;/h2&gt;
&lt;p&gt;The goal wasn&apos;t to open the case. The challenge was to start from what anyone has —the low-privilege sticker credentials and LAN access— and see how far it goes. It goes all the way to root, by chaining four management-plane weaknesses.&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;2 2 393.8 626&quot; width=&quot;394&quot; height=&quot;626&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-router-cadena-en-t fig-router-cadena-en-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 394px&quot;&gt;&lt;title id=&quot;fig-router-cadena-en-t&quot;&gt;Conceptual chain: from a low-privilege local account, V1 broken authorization lets it rewrite its own ACL, V2 firmware exfiltration reads raw blocks over the network, offline analysis of the squashfs yields /etc/shadow, the factory root hash in DES crypt is cracked offline, and it reaches a root shell over SSH with key-based persistence&lt;/title&gt;&lt;desc id=&quot;fig-router-cadena-en-d&quot;&gt;Low-privilege local account sticker credentials · LAN only → V1 · Broken authorization the account rewrites its own ACL; V1 · Broken authorization the account rewrites its own ACL → V2 · Firmware exfiltration raw block-device read over the network; V2 · Firmware exfiltration raw block-device read over the network → Offline analysis squashfs → /etc/shadow; Offline analysis squashfs → /etc/shadow → Factory root hash in DES crypt obsolete → cracked offline; Factory root hash in DES crypt obsolete → cracked offline → Root shell over SSH V3 · key-based persistence&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-router-cadena-en-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-router-cadena-en-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,68 L198.9,87 Q198.9,95 198.9,103 L198.9,122&quot; marker-end=&quot;url(#fig-router-cadena-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,178 L198.9,197 Q198.9,205 198.9,213 L198.9,232&quot; marker-end=&quot;url(#fig-router-cadena-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,288 L198.9,307 Q198.9,315 198.9,323 L198.9,342&quot; marker-end=&quot;url(#fig-router-cadena-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,398 L198.9,417 Q198.9,425 198.9,433 L198.9,452&quot; marker-end=&quot;url(#fig-router-cadena-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,508 L198.9,527 Q198.9,535 198.9,543 L198.9,562&quot; marker-end=&quot;url(#fig-router-cadena-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;48.4&quot; y=&quot;12&quot; width=&quot;301&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;31&quot;&gt;Low-privilege local account&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;49&quot;&gt;sticker credentials · LAN only&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;39.3&quot; y=&quot;122&quot; width=&quot;319.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;141&quot;&gt;V1 · Broken authorization&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;159&quot;&gt;the account rewrites its own ACL&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;12&quot; y=&quot;232&quot; width=&quot;373.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;251&quot;&gt;V2 · Firmware exfiltration&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;269&quot;&gt;raw block-device read over the network&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;84.8&quot; y=&quot;342&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;361&quot;&gt;Offline analysis&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;379&quot;&gt;squashfs → /etc/shadow&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;48.4&quot; y=&quot;452&quot; width=&quot;301&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;471&quot;&gt;Factory root hash in DES crypt&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;489&quot;&gt;obsolete → cracked offline&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;66.6&quot; y=&quot;562&quot; width=&quot;264.6&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;581&quot;&gt;Root shell over SSH&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;599&quot;&gt;V3 · key-based persistence&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;The chain, at the vulnerability-class level: no commands, just the mechanism&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;V1 · The authorization plane breaks itself.&lt;/strong&gt; The first weakness is the most elegant and the gravest. In &lt;code&gt;rpcd&lt;/code&gt;, each login has a permission list. The low-privilege account belongs, by factory configuration, to a group that lets it &lt;strong&gt;write the very configuration that defines its permissions&lt;/strong&gt;. Put another way: &lt;em&gt;a restricted account can edit the rules that restrict it&lt;/em&gt; and promote itself to wildcard permissions. No memory exploit needed; it&apos;s an authorization-model failure. It&apos;s also the part the project itself prepared for publication, because it&apos;s the lesson: the privilege boundary was drawn wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V2 · Exfiltrate the firmware over the network.&lt;/strong&gt; With broad permissions, the next step is to get the firmware, of which there&apos;s no public copy. No UART needed: an authenticated download endpoint of the web stack itself serves whole files &lt;strong&gt;including raw block devices&lt;/strong&gt;, which lets you dump entire flash partitions over HTTPS. It&apos;s a class of flaw —file/block reads beyond what&apos;s intended— that turns an authenticated session into firmware and NVRAM exfiltration. (The exact request stays out: that&apos;s the weaponizable part.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The offline analysis, and the prize: DES crypt in 2026.&lt;/strong&gt; With the &lt;code&gt;rootfs&lt;/code&gt; in hand (a compressed &lt;code&gt;squashfs&lt;/code&gt;), you extract &lt;code&gt;/etc/shadow&lt;/code&gt;. And there&apos;s the finding a security reader spots instantly: the factory &lt;code&gt;root&lt;/code&gt; password hash uses &lt;strong&gt;traditional DES crypt&lt;/strong&gt; —the 1970s scheme, 56-bit, only 8 significant characters—, obsolete for decades. It cracks offline without effort. A current-generation router in 2026 storing its root hash in DES is a first-order hygiene failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V · The wall wasn&apos;t the password.&lt;/strong&gt; An instructive detail: having the root password wasn&apos;t enough, because the SSH port timed out. Not because of crypto or the password, but because that hidden vendor layer (&lt;code&gt;cxt_acl_lan&lt;/code&gt;) had the SSH service &lt;strong&gt;disabled by policy&lt;/strong&gt;, outside the visible firewall. The owner enables it from the vendor&apos;s access control and port 22 opens. Lesson: there&apos;s security-relevant state the interface doesn&apos;t show you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V3 · Clean persistence.&lt;/strong&gt; Oddly, changing root&apos;s password from the interface fails (&quot;unauthorized user&quot;), but file writing is available with the elevated permissions. So persistence was done the standard OpenWrt way: &lt;strong&gt;an SSH public key in root&apos;s &lt;code&gt;authorized_keys&lt;/code&gt;&lt;/strong&gt;. Key-based login, without touching &lt;code&gt;/etc/shadow&lt;/code&gt;, and with the &lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;user&lt;/code&gt; accounts intact. Reversible by deleting a file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V4 · The CLI shortcut.&lt;/strong&gt; And in case that wasn&apos;t enough, the vendor CLI stores its privileged-mode (&quot;enable&quot;) password as a &lt;strong&gt;double base64&lt;/strong&gt; in config, readable by the low-privilege account. That isn&apos;t encryption, it&apos;s obfuscation: it decodes trivially. A vendor secret stored with reversible encoding is, in practice, a cleartext secret.&lt;/p&gt;
&lt;h3 id=&quot;what-this-means&quot;&gt;What this means&lt;/h3&gt;
&lt;p&gt;In classes: &lt;strong&gt;broken authorization&lt;/strong&gt; (V1), &lt;strong&gt;firmware exfiltration&lt;/strong&gt; by block reads (V2), &lt;strong&gt;weak hash and default password&lt;/strong&gt; (DES crypt), &lt;strong&gt;hidden control layer&lt;/strong&gt; (&lt;code&gt;cxt_acl_lan&lt;/code&gt;) and &lt;strong&gt;obfuscated, not encrypted, vendor secret&lt;/strong&gt; (V4). None requires physical access. Anyone with the sticker credentials and the LAN can, in principle, pull the whole firmware, the NVRAM, and get persistent root. And since the operator manages the device over TR-069, the remote-management plane is as critical as the local one.&lt;/p&gt;
&lt;h2 id=&quot;what-we-did-next-a-neutral-hardened-router&quot;&gt;What we did next: a neutral, hardened router&lt;/h2&gt;
&lt;p&gt;Finding root is the fun half. The useful half is taking back control of the device and leaving it fast, robust and under your command, without breaking fiber. Everything that follows is reversible and designed to survive a reboot.&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;2 2 1290.8 422.9&quot; width=&quot;1291&quot; height=&quot;423&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-router-neutro-en-t fig-router-neutro-en-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 1291px&quot;&gt;&lt;title id=&quot;fig-router-neutro-en-t&quot;&gt;Final state: the hardened GRG with TR-069 removed so the ISP stops re-provisioning, a reversible debloat of about 22 services, the HW NAT/PPE kept with no SQM or BBR; and to the question of flashing own firmware on the GRG, because the PON/OMCI stack is closed on the EN7581 the alternatives are an 8311 SFP ONT stick in your router or bridge mode&lt;/title&gt;&lt;desc id=&quot;fig-router-neutro-en-d&quot;&gt;Hardened GRG-4355u own root · reversible → TR-069 removed the ISP stops re-provisioning; Hardened GRG-4355u own root · reversible → Debloat (~22 services) VoIP, DLNA, UPnP, telemetry…; Hardened GRG-4355u own root · reversible → HW NAT/PPE kept no SQM/BBR on purpose; Hardened GRG-4355u own root · reversible → Own firmware on the GRG? closed PON/OMCI on EN7581; Own firmware on the GRG? closed PON/OMCI on EN7581 → SFP ONT stick (8311) in your own router (not viable → alternatives); Own firmware on the GRG? closed PON/OMCI on EN7581 → Bridge mode (VLAN20 → LAN5) your router behind the GRG&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-router-neutro-en-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-router-neutro-en-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;343.4&quot; y=&quot;152.9&quot; width=&quot;315.9&quot; height=&quot;262&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M152.6,279.9 L252.2,369.5 Q258.1,374.9 266.1,374.9 L277.1,374.9 Q285.1,374.9 293.1,374.9 L355.4,374.9&quot; marker-end=&quot;url(#fig-router-neutro-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M214.8,279.9 L250.4,290.6 Q258.1,292.9 266.1,292.9 L277.1,292.9 Q285.1,292.9 293.1,292.9 L359.9,292.9&quot; marker-end=&quot;url(#fig-router-neutro-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M214.8,223.9 L250.4,213.2 Q258.1,210.9 266.1,210.9 L277.1,210.9 Q285.1,210.9 293.1,210.9 L391.8,210.9&quot; marker-end=&quot;url(#fig-router-neutro-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M143.9,223.9 L253.1,87.2 Q258.1,81 266.1,81 L277.1,81 Q285.1,81 293.1,81 L312.1,81&quot; marker-end=&quot;url(#fig-router-neutro-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M690.6,116.9 L709.7,120.5 Q717.6,122 725.6,122 L855.3,122 Q863.3,122 871.3,122 L1040.9,122&quot; marker-end=&quot;url(#fig-router-neutro-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;744.6&quot; y=&quot;111&quot; width=&quot;237.5&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;863.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;863.3&quot; y=&quot;122&quot;&gt;not viable → alternatives&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M690.6,45.1 L709.7,41.5 Q717.6,40 725.6,40 L855.3,40 Q863.3,40 871.3,40 L1009.1,40&quot; marker-end=&quot;url(#fig-router-neutro-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;349.4&quot; y=&quot;158.9&quot; width=&quot;164.7&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;354.4&quot; y=&quot;166.9&quot; dominant-baseline=&quot;central&quot;&gt;Reversible hardening&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;12&quot; y=&quot;223.9&quot; width=&quot;219.1&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;121.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;121.6&quot; y=&quot;242.9&quot;&gt;Hardened GRG-4355u&lt;/tspan&gt;&lt;tspan x=&quot;121.6&quot; y=&quot;260.9&quot;&gt;own root · reversible&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;355.4&quot; y=&quot;346.9&quot; width=&quot;291.9&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;501.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;365.9&quot;&gt;TR-069 removed&lt;/tspan&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;383.9&quot;&gt;the ISP stops re-provisioning&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;359.9&quot; y=&quot;264.9&quot; width=&quot;282.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;501.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;283.9&quot;&gt;Debloat (~22 services)&lt;/tspan&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;301.9&quot;&gt;VoIP, DLNA, UPnP, telemetry…&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;391.8&quot; y=&quot;182.9&quot; width=&quot;219.1&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;501.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;201.9&quot;&gt;HW NAT/PPE kept&lt;/tspan&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;219.9&quot;&gt;no SQM/BBR on purpose&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;501.3,33.2 690.6,81 501.3,128.8 312.1,81&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;501.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;72&quot;&gt;Own firmware on the GRG?&lt;/tspan&gt;&lt;tspan x=&quot;501.3&quot; y=&quot;90&quot;&gt;closed PON/OMCI on EN7581&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;1040.9&quot; y=&quot;94&quot; width=&quot;210&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;1145.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;1145.9&quot; y=&quot;113&quot;&gt;SFP ONT stick (8311)&lt;/tspan&gt;&lt;tspan x=&quot;1145.9&quot; y=&quot;131&quot;&gt;in your own router&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;1009.1&quot; y=&quot;12&quot; width=&quot;273.7&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;1145.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;1145.9&quot; y=&quot;31&quot;&gt;Bridge mode (VLAN20 → LAN5)&lt;/tspan&gt;&lt;tspan x=&quot;1145.9&quot; y=&quot;49&quot;&gt;your router behind the GRG&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;The hardened router, and why own firmware doesn&apos;t go on the GRG&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Autonomy: TR-069 out.&lt;/strong&gt; First, remove the TR-069/CWMP agent (&lt;code&gt;easycwmp&lt;/code&gt; and the related packages) and disable its daemons. With that, &lt;strong&gt;the operator stops managing, updating and re-provisioning the device automatically&lt;/strong&gt;. It&apos;s the change that returns the most control: the router now boots from its saved configuration, with no ACS deciding for you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reversible debloat.&lt;/strong&gt; A home router ships with a pile of services you don&apos;t use and that are attack surface: VoIP, DLNA/Samba/FTP, multicast proxy, UPnP, DDNS, SNMP, assorted telemetry. The optimization script turns off around &lt;strong&gt;22 services&lt;/strong&gt;, saving state so it can be reverted. Key: it&apos;s done with an allowlist of what&apos;s &lt;strong&gt;never&lt;/strong&gt; touched —the PON stack (the fiber), the firewall, &lt;code&gt;dropbear&lt;/code&gt;, &lt;code&gt;rpcd&lt;/code&gt;, &lt;code&gt;uhttpd&lt;/code&gt;, DNS/DHCP, NTP— so nothing essential breaks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The optimization we deliberately &lt;em&gt;didn&apos;t&lt;/em&gt; do.&lt;/strong&gt; Here&apos;s the interesting engineering call: &lt;strong&gt;no SQM, no BBR/buffer tuning&lt;/strong&gt;. It sounds counterintuitive, but the EN7581 does NAT in hardware (HW NAT/PPE); adding software queues or rewriting buffers &lt;strong&gt;would pull traffic off the hardware path and make it slower&lt;/strong&gt;. The AQM is already done by the chip&apos;s own queues. The only network tuning was raising &lt;code&gt;conntrack_max&lt;/code&gt; (from 16,384 to 65,536) and lowering the console log level. Technical honesty: the CPU governor change was noted but on the EN7581 turns out to be a &lt;em&gt;no-op&lt;/em&gt; (it stays &lt;code&gt;userspace&lt;/code&gt;), so it doesn&apos;t count as an improvement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Robustness and persistence.&lt;/strong&gt; The changes live in the &lt;em&gt;overlay&lt;/em&gt; (UBIFS), so they &lt;strong&gt;survive a reboot&lt;/strong&gt;. A factory reset does wipe them —by the device&apos;s design—, which is why the project has an automated re-entry that puts the router back into its known state without manual work. Every change has its rollback procedure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The neutral router.&lt;/strong&gt; The end goal was to put &lt;em&gt;your&lt;/em&gt; router behind it and use the GRG only as an ONT. You can, by bridging the Internet VLAN (20) to the 10 Gb port (LAN5): the GRG acts as a dumb ONT and your router routes. In our setup, a second box also ended up as a dumb access point for Wi-Fi.&lt;/p&gt;
&lt;h2 id=&quot;and-own-firmware-the-honest-verdict&quot;&gt;And own firmware? The honest verdict&lt;/h2&gt;
&lt;p&gt;The natural question for anyone who gets this far: &lt;em&gt;why not flash real OpenWrt and forget the vendor firmware?&lt;/em&gt; The answer, after evaluating it, is that &lt;strong&gt;today it isn&apos;t viable on the GRG&lt;/strong&gt; if you want to keep fiber.&lt;/p&gt;
&lt;p&gt;The reason is the SoC. The EN7581 is both the router and the &lt;strong&gt;ONT&lt;/strong&gt; (the optical part), and the whole fiber stack —the PON MAC kernel modules, Airoha&apos;s network daemon, the OMCI stack— is &lt;strong&gt;closed binary&lt;/strong&gt; and tied to kernel 5.4.55. Upstream OpenWrt has basic EN7581 support (Ethernet, Wi-Fi, routing) but &lt;strong&gt;does no PON&lt;/strong&gt;: flashing it means losing fiber. Reusing the vendor blobs on a modern OpenWrt doesn&apos;t work either: the 5.4.55 modules won&apos;t load on 6.x kernels and nobody has ported the EN7581 PON stack.&lt;/p&gt;
&lt;p&gt;The realistic alternatives, then, don&apos;t go through the GRG:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An &quot;8311&quot; SFP ONT stick&lt;/strong&gt; (the ONT in a transceiver) in your own router&apos;s SFP cage, programmed with the line&apos;s identity. Your router is the router; the operator only controls the stick over OMCI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bridge mode&lt;/strong&gt;, which is what we left set up: the GRG as a bridged ONT with your router behind it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Saying &quot;you can&apos;t, and here&apos;s what you can&quot; is part of the craft. An honest result is worth more than a promise that bricks the fiber.&lt;/p&gt;
&lt;h2 id=&quot;threat-model-and-what-we-take-away&quot;&gt;Threat model and what we take away&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The management plane rules everything else.&lt;/strong&gt; It doesn&apos;t matter how strong the Wi-Fi password is if a local account can rewrite its own permissions and the operator can push config over TR-069. A router&apos;s security is decided by who controls its management, local and remote.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Defaults at fleet scale are the real risk.&lt;/strong&gt; A trivial password and a root hash in DES crypt, if common across the fleet, expose every unit to the same local compromise. DES in 2026 is a hygiene failure that&apos;s hard to justify.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Obfuscation isn&apos;t encryption.&lt;/strong&gt; A double base64 &quot;protecting&quot; a vendor secret is a cleartext secret with an extra step. The same goes for any &quot;encoding&quot; an attacker undoes in their head.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There are control planes you can&apos;t see.&lt;/strong&gt; A vendor firewall outside UCI, or a TR-069 you can&apos;t turn off from the interface, mean you don&apos;t have full visibility of your own device&apos;s security state. Taking it back —removing TR-069, mapping &lt;code&gt;cxt_acl&lt;/code&gt;— is part of hardening.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Research your own, and with a brake.&lt;/strong&gt; Own equipment, own line, LAN, reversible changes, and no publishing what arms a third party. A guardrail stopping your root login so a human runs it isn&apos;t an obstacle: it&apos;s the responsibility model working.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The living-room router turned out to be a complete security target: broken authorization, firmware exfiltrable over the network, 1970s cryptography and an operator with a control channel the owner can&apos;t see. Turning it into a neutral, fast device under your command was the reward; the trust map —and who&apos;s really in charge— is what we take away.&lt;/p&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OpenWrt&lt;/strong&gt; — the system the vendor firmware is built on and the reference alternative: &lt;a href=&quot;https://openwrt.org&quot;&gt;openwrt.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The 8311 project&lt;/strong&gt; — firmware for SFP ONT sticks, the realistic path to your &quot;own router&quot; on fiber: look for the 8311 community.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;TR-069 / CWMP&lt;/strong&gt; and remote CPE management: the Broadband Forum standard (TR-069).&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>sec</category></item><item><title>¿De quién es tu router? Ingeniería inversa de un Comtrend GRG-4355u de Digi</title><link>https://quantarmy.com/ingenieria-inversa-router-digi-comtrend-grg-4355u/</link><guid isPermaLink="true">https://quantarmy.com/ingenieria-inversa-router-digi-comtrend-grg-4355u/</guid><description>El router que te deja el ISP es un pequeño OpenWrt que no controlas del todo: una cuenta de bajo privilegio que puede reescribir sus propios permisos, un hash root en DES, y un agente TR-069 por el que el operador reconfigura el aparato a distancia. Esto es la ingeniería inversa, lo que encontramos y cómo recuperamos el control, sin secretos ni recetas.</description><pubDate>Wed, 04 Nov 2026 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;El router que te deja el operador no es tuyo del todo. Lo pagas en la cuota, vive en tu casa y lleva tu tráfico, pero su firmware lo decide el ISP, hay cuentas que no ves, y por un canal de gestión remota el operador puede reconfigurarlo, actualizarlo o reaprovisionarlo cuando quiera. Para un lector de ciberseguridad eso es una pregunta de modelo de amenazas: &lt;strong&gt;¿quién controla de verdad este aparato, y qué puede hacer un atacante que tenga las credenciales de la pegatina?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Este artículo responde a esa pregunta sobre un caso concreto: el &lt;strong&gt;Comtrend GRG-4355u&lt;/strong&gt; que &lt;strong&gt;Digi&lt;/strong&gt; distribuye para fibra. Le hicimos ingeniería inversa —sin abrir la caja, sin UART, sin JTAG, solo por la red local—, encontramos una cadena de debilidades que lleva de una cuenta de invitado a root, y después lo convertimos en un router neutro y endurecido que ya no obedece al operador. Todo sobre equipo y línea propios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nota de investigación responsable.&lt;/strong&gt; Todo lo que sigue es sobre un aparato propio, en la línea propia, en la LAN, con cambios reversibles y rollback documentado. No publicamos ni una credencial, ni un hash, ni un número de serie, ni la receta paso a paso: esto es un análisis de &lt;strong&gt;clases de vulnerabilidad y su significado&lt;/strong&gt;, no un arma contra el parque de un ISP. Un detalle que vale la pena contar: el propio guardrail de la IA con la que trabajamos se negó a ejecutar el login root interactivo, y esos pasos los hizo el humano. La seguridad también se hace así.&lt;/p&gt;
&lt;h2 id=&quot;el-ecosistema-un-openwrt-del-fabricante-con-dueños-compartidos&quot;&gt;El ecosistema: un OpenWrt del fabricante con dueños compartidos&lt;/h2&gt;
&lt;p&gt;Antes de tocar nada, el inventario. El GRG-4355u lo fabrica &lt;strong&gt;Comtrend&lt;/strong&gt; (aunque Digi lo distribuya), monta un SoC &lt;strong&gt;Airoha EN7581&lt;/strong&gt; (cuatro núcleos ARM Cortex-A53), y por dentro corre un &lt;strong&gt;OpenWrt del fabricante&lt;/strong&gt; con interfaz &lt;strong&gt;LuCI&lt;/strong&gt;, kernel &lt;strong&gt;5.4.55&lt;/strong&gt;, bajo la etiqueta de firmware &lt;strong&gt;DIG-1.3.22&lt;/strong&gt;. Las unidades que hemos visto declaran hardware 1.1 o 1.2.&lt;/p&gt;
&lt;p&gt;Lo interesante no es la foto del hardware, sino el mapa de quién manda sobre él:&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;10 2 1080.9 371&quot; width=&quot;1081&quot; height=&quot;371&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-router-ecosistema-es-t fig-router-ecosistema-es-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 1081px&quot;&gt;&lt;title id=&quot;fig-router-ecosistema-es-t&quot;&gt;Planos de gestión del Comtrend GRG-4355u: el SoC EN7581 corre firmware DIG-1.3.22 (OpenWrt del fabricante con LuCI); en la LAN conviven la web LuCI sobre HTTPS con rpcd/ubus, SSH que cae en un CLI del fabricante, y cxt_acl_lan, un firewall propio fuera del UCI; por encima, el ACS del ISP gestiona el equipo por TR-069 sobre la VLAN de gestión&lt;/title&gt;&lt;desc id=&quot;fig-router-ecosistema-es-d&quot;&gt;Comtrend GRG-4355u (HGU de fibra) SoC Airoha EN7581 · quad Cortex-A53 → Firmware DIG-1.3.22 OpenWrt del fabricante + LuCI · kernel 5.4.55; Firmware DIG-1.3.22 OpenWrt del fabricante + LuCI · kernel 5.4.55 → LuCI / HTTPS rpcd · ubus (JSON-RPC); Firmware DIG-1.3.22 OpenWrt del fabricante + LuCI · kernel 5.4.55 → dropbear SSH la cuenta cae en /sbin/cli, no en shell; Firmware DIG-1.3.22 OpenWrt del fabricante + LuCI · kernel 5.4.55 → cxt_acl_lan firewall del fabricante, fuera del UCI; TR-069 / CWMP el ACS del ISP gestiona el equipo → Firmware DIG-1.3.22 OpenWrt del fabricante + LuCI · kernel 5.4.55 (VLAN de gestión · config, firmware, credenciales)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-router-ecosistema-es-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-router-ecosistema-es-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;20&quot; y=&quot;265&quot; width=&quot;1060.9&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M789,75 L789,112 Q789,120 781.6,123 L693.6,158&quot; marker-end=&quot;url(#fig-router-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M792,214 L946.9,239.7 Q954.8,241 954.8,249 L954.8,260 Q954.8,268 954.8,276 L954.8,295&quot; marker-end=&quot;url(#fig-router-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M623.3,214 L623.3,233 Q623.3,241 623.3,249 L623.3,260 Q623.3,268 623.3,276 L623.3,295&quot; marker-end=&quot;url(#fig-router-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M417.4,214 L226.8,239.9 Q218.9,241 218.9,249 L218.9,260 Q218.9,268 218.9,276 L218.9,295&quot; marker-end=&quot;url(#fig-router-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M425.6,82 L425.6,112 Q425.6,120 433.2,122.5 L539.4,158&quot; marker-end=&quot;url(#fig-router-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;202.2&quot; y=&quot;109&quot; width=&quot;446.8&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;425.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;425.6&quot; y=&quot;120&quot;&gt;VLAN de gestión · config, firmware, credenciales&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;26&quot; y=&quot;271&quot; width=&quot;118.3&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;31&quot; y=&quot;279&quot; dominant-baseline=&quot;central&quot;&gt;Gestión en LAN&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;615.8&quot; y=&quot;19&quot; width=&quot;346.5&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;789&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;789&quot; y=&quot;38&quot;&gt;Comtrend GRG-4355u (HGU de fibra)&lt;/tspan&gt;&lt;tspan x=&quot;789&quot; y=&quot;56&quot;&gt;SoC Airoha EN7581 · quad Cortex-A53&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;404.5&quot; y=&quot;158&quot; width=&quot;437.5&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;623.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;623.3&quot; y=&quot;177&quot;&gt;Firmware DIG-1.3.22&lt;/tspan&gt;&lt;tspan x=&quot;623.3&quot; y=&quot;195&quot;&gt;OpenWrt del fabricante + LuCI · kernel 5.4.55&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;840.7&quot; y=&quot;295&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;954.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;954.8&quot; y=&quot;314&quot;&gt;LuCI / HTTPS&lt;/tspan&gt;&lt;tspan x=&quot;954.8&quot; y=&quot;332&quot;&gt;rpcd · ubus (JSON-RPC)&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;431.8&quot; y=&quot;295&quot; width=&quot;382.9&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;623.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;623.3&quot; y=&quot;314&quot;&gt;dropbear SSH&lt;/tspan&gt;&lt;tspan x=&quot;623.3&quot; y=&quot;332&quot;&gt;la cuenta cae en /sbin/cli, no en shell&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;32&quot; y=&quot;295&quot; width=&quot;373.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;218.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;218.9&quot; y=&quot;314&quot;&gt;cxt_acl_lan&lt;/tspan&gt;&lt;tspan x=&quot;218.9&quot; y=&quot;332&quot;&gt;firewall del fabricante, fuera del UCI&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;g&gt;&lt;path class=&quot;fig-node&quot; d=&quot;M261.5,18 A164.2,6 0 0 1 589.8,18 V76 A164.2,6 0 0 1 261.5,76 Z&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-node-line&quot; d=&quot;M261.5,18 A164.2,6 0 0 0 589.8,18&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;text class=&quot;fig-text&quot; x=&quot;425.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;425.6&quot; y=&quot;42&quot;&gt;TR-069 / CWMP&lt;/tspan&gt;&lt;tspan x=&quot;425.6&quot; y=&quot;60&quot;&gt;el ACS del ISP gestiona el equipo&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;Quién controla qué: tres planos de gestión en LAN y uno remoto del ISP&lt;/figcaption&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cuentas:&lt;/strong&gt; hay un &lt;code&gt;admin&lt;/code&gt;, un &lt;code&gt;user&lt;/code&gt; de bajo privilegio —cuyas credenciales suelen venir en la pegatina— y un &lt;code&gt;root&lt;/code&gt;. Lo llamativo: entrar por SSH con las cuentas normales no da una shell de Linux, sino un &lt;strong&gt;CLI restringido del fabricante&lt;/strong&gt; (&lt;code&gt;/sbin/cli&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El plano web:&lt;/strong&gt; LuCI habla por HTTPS con &lt;code&gt;rpcd&lt;/code&gt;/&lt;code&gt;ubus&lt;/code&gt;, el bus de llamadas de OpenWrt. Ahí se decide, por configuración, qué puede leer y escribir cada login.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Una capa de control oculta:&lt;/strong&gt; además del firewall estándar (UCI), el fabricante instala &lt;code&gt;cxt_acl_lan&lt;/code&gt;, un control de acceso propio &lt;strong&gt;fuera&lt;/strong&gt; del UCI. Es decir, hay reglas que deciden qué puertos están abiertos que &lt;strong&gt;no se ven&lt;/strong&gt; en la configuración normal del cortafuegos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El ISP, por encima de todo:&lt;/strong&gt; un agente &lt;strong&gt;TR-069 / CWMP&lt;/strong&gt; conecta con el servidor de gestión (ACS) del operador por una VLAN de gestión separada. Ese canal puede empujar configuración, firmware y credenciales. El dueño de la casa no lo apaga desde la interfaz.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esa última pieza es la tesis del artículo: el eslabón de confianza más fuerte del aparato no es local, es del operador.&lt;/p&gt;
&lt;h2 id=&quot;ingeniería-inversa-de-invitado-a-root-por-la-red&quot;&gt;Ingeniería inversa: de invitado a root, por la red&lt;/h2&gt;
&lt;p&gt;El objetivo no era abrir la caja. El reto era partir de lo que tiene cualquiera —las credenciales de bajo privilegio de la pegatina y acceso a la LAN— y ver hasta dónde se llega. Se llega a root, encadenando cuatro debilidades del plano de gestión.&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;2 2 393.8 626&quot; width=&quot;394&quot; height=&quot;626&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-router-cadena-es-t fig-router-cadena-es-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 394px&quot;&gt;&lt;title id=&quot;fig-router-cadena-es-t&quot;&gt;Cadena conceptual: desde una cuenta local de bajo privilegio, V1 autorización rota permite reescribir la propia ACL, V2 exfiltración de firmware lee bloques crudos por la red, el análisis offline del squashfs da el /etc/shadow, el hash root de fábrica en DES crypt se rompe offline, y se llega a shell root por SSH con persistencia por clave&lt;/title&gt;&lt;desc id=&quot;fig-router-cadena-es-d&quot;&gt;Cuenta local de bajo privilegio credenciales de la pegatina · solo LAN → V1 · Autorización rota la cuenta reescribe su propia ACL; V1 · Autorización rota la cuenta reescribe su propia ACL → V2 · Exfiltración de firmware lectura de bloques crudos por la red; V2 · Exfiltración de firmware lectura de bloques crudos por la red → Análisis offline squashfs → /etc/shadow; Análisis offline squashfs → /etc/shadow → Hash root de fábrica en DES crypt obsoleto → se rompe offline; Hash root de fábrica en DES crypt obsoleto → se rompe offline → Shell root por SSH V3 · persistencia por clave&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-router-cadena-es-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-router-cadena-es-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,68 L198.9,87 Q198.9,95 198.9,103 L198.9,122&quot; marker-end=&quot;url(#fig-router-cadena-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,178 L198.9,197 Q198.9,205 198.9,213 L198.9,232&quot; marker-end=&quot;url(#fig-router-cadena-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,288 L198.9,307 Q198.9,315 198.9,323 L198.9,342&quot; marker-end=&quot;url(#fig-router-cadena-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,398 L198.9,417 Q198.9,425 198.9,433 L198.9,452&quot; marker-end=&quot;url(#fig-router-cadena-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M198.9,508 L198.9,527 Q198.9,535 198.9,543 L198.9,562&quot; marker-end=&quot;url(#fig-router-cadena-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;12&quot; y=&quot;12&quot; width=&quot;373.8&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;31&quot;&gt;Cuenta local de bajo privilegio&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;49&quot;&gt;credenciales de la pegatina · solo LAN&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;34.8&quot; y=&quot;122&quot; width=&quot;328.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;141&quot;&gt;V1 · Autorización rota&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;159&quot;&gt;la cuenta reescribe su propia ACL&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;21.1&quot; y=&quot;232&quot; width=&quot;355.6&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;251&quot;&gt;V2 · Exfiltración de firmware&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;269&quot;&gt;lectura de bloques crudos por la red&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;84.8&quot; y=&quot;342&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;361&quot;&gt;Análisis offline&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;379&quot;&gt;squashfs → /etc/shadow&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;34.8&quot; y=&quot;452&quot; width=&quot;328.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;471&quot;&gt;Hash root de fábrica en DES crypt&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;489&quot;&gt;obsoleto → se rompe offline&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;62.1&quot; y=&quot;562&quot; width=&quot;273.7&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;198.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;581&quot;&gt;Shell root por SSH&lt;/tspan&gt;&lt;tspan x=&quot;198.9&quot; y=&quot;599&quot;&gt;V3 · persistencia por clave&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;La cadena, a nivel de clases de vulnerabilidad: ningún comando, solo el mecanismo&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;V1 · El plano de autorización se rompe a sí mismo.&lt;/strong&gt; La primera debilidad es la más elegante y la más grave. En &lt;code&gt;rpcd&lt;/code&gt;, cada login tiene una lista de permisos. La cuenta de bajo privilegio pertenece, por configuración de fábrica, a un grupo que le permite &lt;strong&gt;escribir la propia configuración que define sus permisos&lt;/strong&gt;. Dicho de otro modo: &lt;em&gt;una cuenta restringida puede editar las reglas que la restringen&lt;/em&gt; y ascenderse a permisos de comodín. No hace falta ningún exploit de memoria; es un fallo de modelo de autorización. Es, además, la parte que el propio proyecto dejó preparada para publicación, porque es la lección: el límite de privilegios estaba mal dibujado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V2 · Exfiltrar el firmware por la red.&lt;/strong&gt; Con permisos amplios, el siguiente paso es conseguir el firmware, del que no hay copia pública. No hace falta un UART: un endpoint de descarga autenticado del propio sistema web sirve ficheros completos &lt;strong&gt;incluyendo dispositivos de bloque crudos&lt;/strong&gt;, lo que permite volcar las particiones de flash enteras por HTTPS. Es una clase de fallo —lectura de ficheros/bloques más allá de lo previsto— que convierte una sesión autenticada en una exfiltración de firmware y NVRAM. (El detalle exacto de la petición queda fuera: es la parte armable.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El análisis offline, y el premio: DES crypt en 2026.&lt;/strong&gt; Con el &lt;code&gt;rootfs&lt;/code&gt; en la mano (un &lt;code&gt;squashfs&lt;/code&gt; comprimido), se extrae &lt;code&gt;/etc/shadow&lt;/code&gt;. Y ahí aparece el hallazgo que un lector de seguridad reconoce al instante: el hash de la contraseña &lt;code&gt;root&lt;/code&gt; de fábrica usa &lt;strong&gt;DES crypt tradicional&lt;/strong&gt; —el esquema de los años 70, de 56 bits y solo 8 caracteres significativos—, obsoleto desde hace décadas. Se rompe offline sin esfuerzo. Que un router de gama actual, de 2026, guarde su hash root en DES es un fallo de higiene de primer orden.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V · El muro no era la contraseña.&lt;/strong&gt; Un detalle instructivo: tener la contraseña root no bastaba, porque el puerto SSH daba &lt;em&gt;timeout&lt;/em&gt;. No por cifrado ni por la contraseña, sino porque esa capa oculta del fabricante (&lt;code&gt;cxt_acl_lan&lt;/code&gt;) tenía el servicio SSH &lt;strong&gt;desactivado por política&lt;/strong&gt;, fuera del cortafuegos visible. El dueño lo habilita desde el control de acceso del fabricante y el puerto 22 abre. Lección: hay estado de seguridad relevante que la interfaz no te enseña.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V3 · Persistencia limpia.&lt;/strong&gt; Curiosamente, cambiar la contraseña de root desde la interfaz falla (&quot;usuario no autorizado&quot;), pero la escritura de ficheros sí está disponible con los permisos elevados. Así que la persistencia se hizo por el camino estándar de OpenWrt: &lt;strong&gt;una clave pública SSH en el &lt;code&gt;authorized_keys&lt;/code&gt; de root&lt;/strong&gt;. Login por clave, sin tocar &lt;code&gt;/etc/shadow&lt;/code&gt;, y con las cuentas &lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;user&lt;/code&gt; intactas. Reversible borrando un fichero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;V4 · El atajo del CLI.&lt;/strong&gt; Y por si faltaba, el CLI del fabricante guarda su contraseña de modo privilegiado (&quot;enable&quot;) como un &lt;strong&gt;doble base64&lt;/strong&gt; en la configuración, legible por la cuenta de bajo privilegio. Eso no es cifrado, es ofuscación: se decodifica de forma trivial. Un secreto de fabricante guardado con codificación reversible es, en la práctica, un secreto en claro.&lt;/p&gt;
&lt;h3 id=&quot;lo-que-esto-significa&quot;&gt;Lo que esto significa&lt;/h3&gt;
&lt;p&gt;Resumido en clases: &lt;strong&gt;autorización rota&lt;/strong&gt; (V1), &lt;strong&gt;exfiltración de firmware&lt;/strong&gt; por lectura de bloques (V2), &lt;strong&gt;hash débil y contraseña por defecto&lt;/strong&gt; (DES crypt), &lt;strong&gt;capa de control oculta&lt;/strong&gt; (&lt;code&gt;cxt_acl_lan&lt;/code&gt;) y &lt;strong&gt;secreto de fabricante ofuscado, no cifrado&lt;/strong&gt; (V4). Ninguna requiere acceso físico. Cualquiera con las credenciales de la pegatina y la LAN puede, en principio, sacar el firmware entero, la NVRAM y obtener root persistente. Y como el aparato lo gestiona el operador por TR-069, el plano de gestión remota es tan crítico como el local.&lt;/p&gt;
&lt;h2 id=&quot;qué-hicimos-después-un-router-neutro-y-endurecido&quot;&gt;Qué hicimos después: un router neutro y endurecido&lt;/h2&gt;
&lt;p&gt;Encontrar root es la mitad entretenida. La mitad útil es recuperar el control del aparato y dejarlo rápido, robusto y bajo tu mando, sin romper la fibra. Todo lo que sigue es reversible y está pensado para sobrevivir a un reinicio.&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;2 2 1454.6 411.6&quot; width=&quot;1455&quot; height=&quot;412&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-router-neutro-es-t fig-router-neutro-es-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 1455px&quot;&gt;&lt;title id=&quot;fig-router-neutro-es-t&quot;&gt;Estado final: el GRG endurecido con TR-069 fuera para que el ISP deje de reaprovisionar, un debloat reversible de unos 22 servicios, el HW NAT/PPE intacto sin SQM ni BBR; y ante la pregunta de poner firmware propio en el GRG, como el stack PON/OMCI es cerrado sobre el EN7581, las alternativas son un stick SFP ONT 8311 en tu router o el modo bridge&lt;/title&gt;&lt;desc id=&quot;fig-router-neutro-es-d&quot;&gt;GRG-4355u endurecido root propio · reversible → TR-069 fuera el ISP deja de reaprovisionar; GRG-4355u endurecido root propio · reversible → Debloat (~22 servicios) VoIP, DLNA, UPnP, telemetría…; GRG-4355u endurecido root propio · reversible → HW NAT/PPE intacto sin SQM/BBR a propósito; GRG-4355u endurecido root propio · reversible → ¿Firmware propio en el GRG? PON/OMCI cerrado sobre EN7581; ¿Firmware propio en el GRG? PON/OMCI cerrado sobre EN7581 → Stick SFP ONT (8311) en tu propio router (no viable → alternativas); ¿Firmware propio en el GRG? PON/OMCI cerrado sobre EN7581 → Modo bridge (VLAN20 → LAN5) tu router detrás del GRG&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-router-neutro-es-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-router-neutro-es-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;549.5&quot; y=&quot;138.8&quot; width=&quot;897.1&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M948.7,54.6 L1280.7,94.1 Q1288.7,95 1288.7,103 L1288.7,114 Q1288.7,122 1288.7,130 L1288.7,168.8&quot; marker-end=&quot;url(#fig-router-neutro-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M899.4,68 L963.3,92.2 Q970.8,95 970.8,103 L970.8,114 Q970.8,122 970.8,130 L970.8,168.8&quot; marker-end=&quot;url(#fig-router-neutro-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M751.5,68 L687.6,92.2 Q680.2,95 680.2,103 L680.2,114 Q680.2,122 680.2,130 L680.2,168.8&quot; marker-end=&quot;url(#fig-router-neutro-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M702.3,52.5 L290.7,94.2 Q282.8,95 282.8,103 L282.8,114 Q282.8,122 282.8,130 L282.8,149&quot; marker-end=&quot;url(#fig-router-neutro-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M368.4,244.6 L409.7,267.7 Q416.7,271.6 416.7,279.6 L416.7,301.6 Q416.7,309.6 416.7,317.6 L416.7,347.6&quot; marker-end=&quot;url(#fig-router-neutro-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;302.5&quot; y=&quot;298.6&quot; width=&quot;228.4&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;416.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;416.7&quot; y=&quot;309.6&quot;&gt;no viable → alternativas&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M197.2,244.6 L155.8,267.7 Q148.9,271.6 148.9,279.6 L148.9,301.6 Q148.9,309.6 148.9,317.6 L148.9,347.6&quot; marker-end=&quot;url(#fig-router-neutro-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;555.5&quot; y=&quot;144.8&quot; width=&quot;203.4&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;560.5&quot; y=&quot;152.8&quot; dominant-baseline=&quot;central&quot;&gt;Endurecimiento reversible&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;702.3&quot; y=&quot;12&quot; width=&quot;246.4&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;825.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;825.5&quot; y=&quot;31&quot;&gt;GRG-4355u endurecido&lt;/tspan&gt;&lt;tspan x=&quot;825.5&quot; y=&quot;49&quot;&gt;root propio · reversible&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;1142.7&quot; y=&quot;168.8&quot; width=&quot;291.9&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;1288.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;1288.7&quot; y=&quot;187.8&quot;&gt;TR-069 fuera&lt;/tspan&gt;&lt;tspan x=&quot;1288.7&quot; y=&quot;205.8&quot;&gt;el ISP deja de reaprovisionar&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;824.8&quot; y=&quot;168.8&quot; width=&quot;291.9&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;970.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;970.8&quot; y=&quot;187.8&quot;&gt;Debloat (~22 servicios)&lt;/tspan&gt;&lt;tspan x=&quot;970.8&quot; y=&quot;205.8&quot;&gt;VoIP, DLNA, UPnP, telemetría…&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;561.5&quot; y=&quot;168.8&quot; width=&quot;237.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;680.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;680.2&quot; y=&quot;187.8&quot;&gt;HW NAT/PPE intacto&lt;/tspan&gt;&lt;tspan x=&quot;680.2&quot; y=&quot;205.8&quot;&gt;sin SQM/BBR a propósito&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;282.8,149 498.4,196.8 282.8,244.6 67.1,196.8&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;282.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;282.8&quot; y=&quot;187.8&quot;&gt;¿Firmware propio en el GRG?&lt;/tspan&gt;&lt;tspan x=&quot;282.8&quot; y=&quot;205.8&quot;&gt;PON/OMCI cerrado sobre EN7581&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;311.7&quot; y=&quot;347.6&quot; width=&quot;210&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;416.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;416.7&quot; y=&quot;366.6&quot;&gt;Stick SFP ONT (8311)&lt;/tspan&gt;&lt;tspan x=&quot;416.7&quot; y=&quot;384.6&quot;&gt;en tu propio router&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;12&quot; y=&quot;347.6&quot; width=&quot;273.7&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;148.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;148.9&quot; y=&quot;366.6&quot;&gt;Modo bridge (VLAN20 → LAN5)&lt;/tspan&gt;&lt;tspan x=&quot;148.9&quot; y=&quot;384.6&quot;&gt;tu router detrás del GRG&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;El router endurecido, y por qué el firmware propio no va en el GRG&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Autonomía: fuera TR-069.&lt;/strong&gt; Lo primero, quitar el agente TR-069/CWMP (&lt;code&gt;easycwmp&lt;/code&gt; y los paquetes asociados) y deshabilitar sus demonios. Con eso, &lt;strong&gt;el operador deja de gestionar, actualizar y reaprovisionar el equipo automáticamente&lt;/strong&gt;. Es el cambio que más devuelve el control: el router pasa a arrancar de su configuración guardada, sin un ACS decidiendo por ti.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debloat reversible.&lt;/strong&gt; Un router doméstico trae un montón de servicios que no usas y que son superficie de ataque: VoIP, DLNA/Samba/FTP, multicast proxy, UPnP, DDNS, SNMP, telemetría varia. El script de optimización apaga alrededor de &lt;strong&gt;22 servicios&lt;/strong&gt;, guardando el estado para poder revertir. Clave: se hace con lista blanca de lo que &lt;strong&gt;nunca&lt;/strong&gt; se toca —el stack PON (la fibra), el firewall, &lt;code&gt;dropbear&lt;/code&gt;, &lt;code&gt;rpcd&lt;/code&gt;, &lt;code&gt;uhttpd&lt;/code&gt;, DNS/DHCP, NTP— para no romper nada esencial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La optimización que &lt;em&gt;no&lt;/em&gt; hicimos, a propósito.&lt;/strong&gt; Aquí la decisión de ingeniería interesante: &lt;strong&gt;nada de SQM ni de tuneos de BBR/buffers&lt;/strong&gt;. Suena contraintuitivo, pero el EN7581 hace NAT por hardware (HW NAT/PPE); meter colas de software o reescribir los buffers &lt;strong&gt;sacaría el tráfico del camino del hardware y lo haría más lento&lt;/strong&gt;. El AQM ya lo hacen las colas del propio chip. La única sintonía de red fue subir el &lt;code&gt;conntrack_max&lt;/code&gt; (de 16.384 a 65.536) y bajar el nivel de log de consola. Honestidad técnica: el cambio de gobernador de CPU se dejó anotado pero en el EN7581 resulta ser un &lt;em&gt;no-op&lt;/em&gt; (se queda en &lt;code&gt;userspace&lt;/code&gt;), así que no cuenta como mejora.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Robustez y persistencia.&lt;/strong&gt; Los cambios viven en el &lt;em&gt;overlay&lt;/em&gt; (UBIFS), así que &lt;strong&gt;sobreviven a un reinicio&lt;/strong&gt;. Un reset de fábrica sí los borra —por diseño del aparato—, y por eso el proyecto tiene una re-entrada automatizada que vuelve a dejar el router en su estado conocido sin intervención manual. Cada cambio tiene su procedimiento de vuelta atrás.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El router neutro.&lt;/strong&gt; El objetivo final era poner &lt;em&gt;tu&lt;/em&gt; router detrás y usar el GRG solo como ONT. Se puede, puenteando la VLAN de Internet (la 20) al puerto de 10 Gb (LAN5): el GRG hace de ONT tonto y tu router hace de router. En nuestro montaje, además, un segundo equipo quedó como punto de acceso tonto para la Wi-Fi.&lt;/p&gt;
&lt;h2 id=&quot;y-firmware-propio-el-veredicto-honesto&quot;&gt;¿Y firmware propio? El veredicto honesto&lt;/h2&gt;
&lt;p&gt;La pregunta natural de cualquiera que llega hasta aquí: &lt;em&gt;¿y por qué no flashear OpenWrt de verdad y olvidarse del firmware del fabricante?&lt;/em&gt; La respuesta, tras evaluarlo, es que &lt;strong&gt;hoy no es viable en el GRG&lt;/strong&gt; si quieres conservar la fibra.&lt;/p&gt;
&lt;p&gt;El motivo es el SoC. El EN7581 es a la vez el router y la &lt;strong&gt;ONT&lt;/strong&gt; (la parte óptica), y todo el stack de fibra —los módulos de kernel del MAC PON, el demonio de red de Airoha, la pila OMCI— es &lt;strong&gt;binario cerrado&lt;/strong&gt; y atado al kernel 5.4.55. OpenWrt upstream tiene soporte básico del EN7581 (Ethernet, Wi-Fi, routing), pero &lt;strong&gt;no hace PON&lt;/strong&gt;: flashearlo significa quedarte sin fibra. Reutilizar los blobs del fabricante sobre un OpenWrt moderno tampoco va: los módulos de 5.4.55 no cargan en kernels 6.x y nadie ha portado la pila PON del EN7581.&lt;/p&gt;
&lt;p&gt;Las alternativas realistas, por tanto, no pasan por el GRG:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Un stick SFP ONT &quot;8311&quot;&lt;/strong&gt; (el ONT en un transceptor) en el puerto SFP de tu propio router, programado con la identidad de la línea. Tu router es el router; el operador solo controla el stick por OMCI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modo bridge&lt;/strong&gt;, que es lo que dejamos montado: el GRG como ONT en puente y tu router detrás.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Decir &quot;no se puede, y esto es lo que sí&quot; es parte del oficio. Un resultado honesto vale más que una promesa que brickea la fibra.&lt;/p&gt;
&lt;h2 id=&quot;modelo-de-amenazas-y-lo-que-nos-llevamos&quot;&gt;Modelo de amenazas y lo que nos llevamos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;El plano de gestión manda sobre todo lo demás.&lt;/strong&gt; Da igual lo fuerte que sea la contraseña Wi-Fi si una cuenta local puede reescribir sus propios permisos y si el operador puede empujar configuración por TR-069. La seguridad de un router se juega en quién controla su gestión, local y remota.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los valores por defecto a escala de parque son el riesgo real.&lt;/strong&gt; Una contraseña trivial y un hash root en DES crypt, si son comunes a toda la flota, exponen a cada unidad al mismo compromiso local. DES en 2026 es un fallo de higiene difícil de justificar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ofuscación no es cifrado.&lt;/strong&gt; Un doble base64 &quot;protegiendo&quot; un secreto de fabricante es un secreto en claro con un paso extra. Lo mismo vale para cualquier &quot;codificación&quot; que un atacante deshace mentalmente.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hay planos de control que no ves.&lt;/strong&gt; Un firewall del fabricante fuera del UCI, o un TR-069 que no puedes apagar desde la interfaz, significan que no tienes visibilidad completa del estado de seguridad de tu propio aparato. Recuperarla —quitando TR-069, mapeando &lt;code&gt;cxt_acl&lt;/code&gt;— es parte del endurecimiento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Investiga sobre lo tuyo, y con freno.&lt;/strong&gt; Equipo propio, línea propia, LAN, cambios reversibles, y sin publicar lo que arma a un tercero. Que un guardrail te pare el login root y lo tenga que hacer el humano no es un estorbo: es el modelo de responsabilidad funcionando.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El router del salón resultó ser un objetivo de seguridad completo: autorización rota, firmware exfiltrable por la red, criptografía de los setenta y un operador con un canal de control que el dueño no ve. Convertirlo en un aparato neutro, rápido y bajo tu mando fue la recompensa; el mapa de confianza —y de quién manda de verdad— es lo que nos llevamos.&lt;/p&gt;
&lt;h2 id=&quot;recursos&quot;&gt;Recursos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OpenWrt&lt;/strong&gt; — el sistema sobre el que se construye el firmware del fabricante y la alternativa de referencia: &lt;a href=&quot;https://openwrt.org&quot;&gt;openwrt.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proyecto 8311&lt;/strong&gt; — firmware para sticks SFP ONT, la vía realista al &quot;router propio&quot; en fibra: busca la comunidad 8311.&lt;/li&gt;
&lt;li&gt;Sobre &lt;strong&gt;TR-069 / CWMP&lt;/strong&gt; y la gestión remota de CPE: el estándar del Broadband Forum (TR-069).&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>sec</category></item><item><title>Turning an e-reader into your own system: reverse-engineering the Kobo Libra 2</title><link>https://quantarmy.com/en/reverse-engineering-kobo-libra-2-plato/</link><guid isPermaLink="true">https://quantarmy.com/en/reverse-engineering-kobo-libra-2-plato/</guid><description>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.</description><pubDate>Wed, 04 Nov 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;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&apos;s eyes because &quot;there&apos;s nothing inside.&quot; And yet it has everything that makes a hardware target interesting: a boot chain with &lt;strong&gt;no secure boot&lt;/strong&gt;, 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.&lt;/p&gt;
&lt;p&gt;This article is the record of what we did to a &lt;strong&gt;Kobo Libra 2&lt;/strong&gt;: 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 &lt;strong&gt;Plato&lt;/strong&gt; —an open-source document reader— into the device&apos;s whole operating system. The public result is called &lt;strong&gt;PlatoX&lt;/strong&gt; and is free software (AGPL-3.0); the personal version, with more things wired in, was called &lt;strong&gt;KoboX&lt;/strong&gt;. We&apos;ll keep the two apart carefully, because security lives in the details and mixing editions is lying with precision.&lt;/p&gt;
&lt;p&gt;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 &quot;integrity&quot; and &quot;authenticity&quot; are not the same word.&lt;/p&gt;
&lt;h2 id=&quot;the-ecosystem-you-had-to-understand-before-touching-anything&quot;&gt;The ecosystem you had to understand before touching anything&lt;/h2&gt;
&lt;p&gt;First rule of any intervention on someone else&apos;s hardware: reconstruct the stock system before changing a single byte. You cannot defend —or break with control— what you don&apos;t understand.&lt;/p&gt;
&lt;p&gt;The device is a &lt;strong&gt;Kobo Libra 2&lt;/strong&gt;, internal name &lt;code&gt;io&lt;/code&gt;, board &lt;strong&gt;E70K10&lt;/strong&gt;, confirmed by reading the PCB table in the firmware&apos;s own &lt;code&gt;kobo_config.sh&lt;/code&gt;. Inside: an &lt;strong&gt;NXP i.MX6SLL&lt;/strong&gt; SoC (ARM Cortex-A9), a Rohm &lt;strong&gt;BD71828&lt;/strong&gt; PMIC, a &lt;strong&gt;Realtek RTL8723DS&lt;/strong&gt; Wi-Fi part over SDIO, a &lt;strong&gt;1264×1680&lt;/strong&gt; e-ink panel and &lt;strong&gt;512 MB&lt;/strong&gt; of RAM. The stock software stack, rebuilt piece by piece from the extracted firmware, is a museum of 2015-era embedded Linux:&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;10 2 927.6 595&quot; width=&quot;928&quot; height=&quot;595&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-kobo-ecosistema-en-t fig-kobo-ecosistema-en-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 928px&quot;&gt;&lt;title id=&quot;fig-kobo-ecosistema-en-t&quot;&gt;Stock boot chain of the Kobo Libra 2: i.MX6SLL hardware, U-Boot with no secure boot, Linux kernel 4.1.15, busybox init and Nickel as the reader; apart, the stock services pickel, udev and wpa_supplicant&lt;/title&gt;&lt;desc id=&quot;fig-kobo-ecosistema-en-d&quot;&gt;Kobo Libra 2 (io / E70K10) NXP i.MX6SLL SoC · RTL8723DS Wi-Fi e-ink 1264×1680 · 512 MB RAM → U-Boot NXP 2016.03-00128 no secure boot, no signature; U-Boot NXP 2016.03-00128 no secure boot, no signature → Linux kernel 4.1.15 Kobo&apos;s GPL sources; Linux kernel 4.1.15 Kobo&apos;s GPL sources → busybox init 1.31.1 /etc/init.d/rcS · glibc 2.19; busybox init 1.31.1 /etc/init.d/rcS · glibc 2.19 → Nickel Qt reader · store · account (normal boot); busybox init 1.31.1 /etc/init.d/rcS · glibc 2.19 → pickel factory-reset · upgrade (starts services)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-kobo-ecosistema-en-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-kobo-ecosistema-en-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;20&quot; y=&quot;489&quot; width=&quot;605.9&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M470.7,86 L470.7,105 Q470.7,113 470.7,121 L470.7,140&quot; marker-end=&quot;url(#fig-kobo-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M470.7,196 L470.7,215 Q470.7,223 470.7,231 L470.7,250&quot; marker-end=&quot;url(#fig-kobo-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M470.7,306 L470.7,325 Q470.7,333 470.7,341 L470.7,360&quot; marker-end=&quot;url(#fig-kobo-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M606.5,416 L782.9,452.4 Q790.8,454 790.8,462 L790.8,484 Q790.8,492 790.8,500 L790.8,519&quot; marker-end=&quot;url(#fig-kobo-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;735.7&quot; y=&quot;443&quot; width=&quot;110.1&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;790.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;790.8&quot; y=&quot;454&quot;&gt;normal boot&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M334.9,416 L158.5,452.4 Q150.7,454 150.7,462 L150.7,484 Q150.7,492 150.7,500 L150.7,519&quot; marker-end=&quot;url(#fig-kobo-ecosistema-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;77.4&quot; y=&quot;443&quot; width=&quot;146.5&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;150.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.7&quot; y=&quot;454&quot;&gt;starts services&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;26&quot; y=&quot;495&quot; width=&quot;118.3&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;31&quot; y=&quot;503&quot; dominant-baseline=&quot;central&quot;&gt;Stock services&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;302&quot; y=&quot;12&quot; width=&quot;337.4&quot; height=&quot;74&quot; rx=&quot;37&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;31&quot;&gt;Kobo Libra 2 (io / E70K10)&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;49&quot;&gt;NXP i.MX6SLL SoC · RTL8723DS Wi-Fi&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;67&quot;&gt;e-ink 1264×1680 · 512 MB RAM&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;329.3&quot; y=&quot;140&quot; width=&quot;282.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;159&quot;&gt;U-Boot NXP 2016.03-00128&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;177&quot;&gt;no secure boot, no signature&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;370.3&quot; y=&quot;250&quot; width=&quot;200.9&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;269&quot;&gt;Linux kernel 4.1.15&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;287&quot;&gt;Kobo&apos;s GPL sources&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;329.3&quot; y=&quot;360&quot; width=&quot;282.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;379&quot;&gt;busybox init 1.31.1&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;397&quot;&gt;/etc/init.d/rcS · glibc 2.19&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;653.9&quot; y=&quot;519&quot; width=&quot;273.7&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;790.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;790.8&quot; y=&quot;538&quot;&gt;Nickel&lt;/tspan&gt;&lt;tspan x=&quot;790.8&quot; y=&quot;556&quot;&gt;Qt reader · store · account&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;32&quot; y=&quot;519&quot; width=&quot;237.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;150.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.7&quot; y=&quot;538&quot;&gt;pickel&lt;/tspan&gt;&lt;tspan x=&quot;150.7&quot; y=&quot;556&quot;&gt;factory-reset · upgrade&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;295.3&quot; y=&quot;528&quot; width=&quot;100.8&quot; height=&quot;38&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;345.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;345.7&quot; y=&quot;547&quot;&gt;udev 142&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;422.1&quot; y=&quot;519&quot; width=&quot;191.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;518&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;518&quot; y=&quot;538&quot;&gt;wpa_supplicant 2.9&lt;/tspan&gt;&lt;tspan x=&quot;518&quot; y=&quot;556&quot;&gt;OpenSSL 1.0.1u&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;The stock stack: from U-Boot to Nickel, without a single signed link&lt;/figcaption&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Boot:&lt;/strong&gt; NXP&apos;s U-Boot (&lt;code&gt;2016.03-00128&lt;/code&gt; on the 4.38 branch), &lt;strong&gt;no signature verification&lt;/strong&gt;. Whoever controls what the kernel runs as init controls the device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel:&lt;/strong&gt; Linux &lt;strong&gt;4.1.15&lt;/strong&gt;, Kobo&apos;s exact build &lt;code&gt;-00868-g58a2758be07&lt;/code&gt;, compiled with GCC 5.3.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Userland:&lt;/strong&gt; &lt;code&gt;busybox 1.31.1&lt;/code&gt; as PID 1, &lt;strong&gt;glibc 2.19&lt;/strong&gt;, libstdc++ from GCC 4.9. udev 142, &lt;code&gt;wpa_supplicant 2.9&lt;/code&gt; over &lt;strong&gt;OpenSSL 1.0.1u&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The reader:&lt;/strong&gt; &lt;strong&gt;Nickel&lt;/strong&gt;, Kobo&apos;s Qt application, with its store and account.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pickel&lt;/code&gt;:&lt;/strong&gt; a shell wrapper that execs &lt;code&gt;pickel-imx&lt;/code&gt; (an ELF). Keep it in mind: &lt;code&gt;pickel&lt;/code&gt; is what decides whether the device factory-resets or accepts an update. Without knowing it, it is a security component.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Storage is an eMMC &lt;code&gt;/dev/mmcblk0&lt;/code&gt; with three partitions and several raw zones outside them:&lt;/p&gt;








































&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Zone&lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;th&gt;Size / position&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mmcblk0p1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;rootfs&lt;/code&gt; (where Nickel runs)&lt;/td&gt;&lt;td&gt;256 MB ext4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mmcblk0p2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;recoveryfs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;384 MB ext4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mmcblk0p3&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;KOBOeReader&lt;/code&gt; (the &quot;onboard&quot;, visible over USB)&lt;/td&gt;&lt;td&gt;29.1 GB vfat&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;zImage&lt;/td&gt;&lt;td&gt;raw kernel&lt;/td&gt;&lt;td&gt;sector 2048&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hwconfig&lt;/td&gt;&lt;td&gt;hardware config&lt;/td&gt;&lt;td&gt;sector 1024&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;waveform&lt;/td&gt;&lt;td&gt;e-ink table&lt;/td&gt;&lt;td&gt;sector 14336&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;256 MB&lt;/code&gt; rootfs and that &lt;code&gt;vfat&lt;/code&gt; user partition are not trivia: both bit us later. The reference firmware was pinned to &lt;strong&gt;4.38.23828&lt;/strong&gt; (the kernel hashes the system accepts are anchored to it).&lt;/p&gt;
&lt;h2 id=&quot;reverse-engineering-process-and-results&quot;&gt;Reverse engineering: process and results&lt;/h2&gt;
&lt;p&gt;With the map in hand, the operational question is a single one: &lt;strong&gt;how do you get your own code in without opening the device or touching U-Boot?&lt;/strong&gt; The answer was inside Kobo&apos;s own update mechanism.&lt;/p&gt;
&lt;h3 id=&quot;the-vector-koboroottgz-and-implicit-trust&quot;&gt;The vector: &lt;code&gt;KoboRoot.tgz&lt;/code&gt; and implicit trust&lt;/h3&gt;
&lt;p&gt;Kobo updates like this: if the stock init finds a &lt;code&gt;.kobo/KoboRoot.tgz&lt;/code&gt; file in storage, it unpacks it over &lt;code&gt;/&lt;/code&gt; as root and reboots. In the stock &lt;code&gt;rcS&lt;/code&gt; that happens around lines 251–269, with &lt;code&gt;pickel can-upgrade&lt;/code&gt; 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 &lt;strong&gt;755&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Put in threat language: &lt;strong&gt;the updater is the vector&lt;/strong&gt;. Anyone who can write to the user partition —which mounts over USB with no credentials— can drop a &lt;code&gt;KoboRoot.tgz&lt;/code&gt; that the system will apply as root on the next boot. The device&apos;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.&lt;/p&gt;
&lt;h3 id=&quot;the-boot-counter-in-the-mbr&quot;&gt;The boot counter in the MBR&lt;/h3&gt;
&lt;p&gt;The prettiest finding of the whole reverse-engineering effort wasn&apos;t in a binary but in five bytes of the MBR. On &lt;code&gt;/dev/mmcblk0&lt;/code&gt;, offsets &lt;strong&gt;0x133–0x137&lt;/strong&gt;, Kobo keeps a &lt;strong&gt;boot counter&lt;/strong&gt;. On each boot, &lt;code&gt;pickel should-factory-reset&lt;/code&gt; increments it; when the index reaches &lt;strong&gt;4&lt;/strong&gt;, the device shows the factory-reset screen and wipes itself. The only thing that resets that counter is &lt;strong&gt;Nickel&lt;/strong&gt;, which writes the sequence &lt;code&gt;8d ef 02 d2 00&lt;/code&gt; when it boots cleanly.&lt;/p&gt;
&lt;p&gt;The security consequence runs deep: &lt;strong&gt;a counter on disk is security state&lt;/strong&gt;, and if your system replaces Nickel, it inherits its obligations. An own system that boots instead of Nickel and doesn&apos;t reset that counter self-destructs in four reboots. We had to replicate Nickel&apos;s exact gesture —write &lt;code&gt;8d ef 02 d2 00&lt;/code&gt; at offset &lt;code&gt;307&lt;/code&gt;, re-read and log— only once Plato had been alive past a time threshold. A mutation test (writing at offset &lt;code&gt;308&lt;/code&gt;) had to fail, and it did. That landed in version 1.0.3, after watching the device wipe itself on the bench.&lt;/p&gt;
&lt;h3 id=&quot;the-bricks-told-as-what-they-are-results&quot;&gt;The bricks, told as what they are: results&lt;/h3&gt;
&lt;p&gt;The honest part of any hardware work is the list of times you left it unusable. Each brick was a finding about the system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Permissions (&lt;code&gt;EACCES&lt;/code&gt;):&lt;/strong&gt; we packaged &lt;code&gt;rcS&lt;/code&gt; as mode &lt;code&gt;644&lt;/code&gt;. &lt;code&gt;busybox init&lt;/code&gt; invokes it with &lt;code&gt;execv&lt;/code&gt;, which requires the execute bit → &lt;code&gt;EACCES&lt;/code&gt; → nothing boots. Fixed by shipping it &lt;code&gt;755&lt;/code&gt; with a &lt;code&gt;check_exec&lt;/code&gt; guard. (fix 1.0.1)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Space (&lt;code&gt;ENOSPC&lt;/code&gt;):&lt;/strong&gt; we put 44.5 MB into a 256 MB rootfs that was already at 93%. &lt;code&gt;etc/&lt;/code&gt; extracted half-way and the device stalled at the splash, never reaching Plato. The fix was to move Plato to the &lt;strong&gt;onboard&lt;/strong&gt; (vfat) and leave the rootfs with a &lt;strong&gt;29 KB&lt;/strong&gt; delta, with a hard limit of ≤ 256 KB on the system partition. (fix 1.0.2)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The self-inflicted factory reset:&lt;/strong&gt; an early &lt;em&gt;debloat&lt;/em&gt; phase put everything under &lt;code&gt;/usr/local/Kobo/*&lt;/code&gt; on the delete list… including &lt;code&gt;pickel-imx&lt;/code&gt;. Next boot: &lt;code&gt;pickel should-factory-reset&lt;/code&gt; returns 127, the system reads that as &quot;must reset&quot;, enters recovery, reflashes firmware &lt;strong&gt;4.33.19747&lt;/strong&gt; and &lt;strong&gt;wipes the onboard&lt;/strong&gt;. Out of that came the project&apos;s single most important rule: a &lt;strong&gt;protect-list&lt;/strong&gt; that never touches &lt;code&gt;pickel*&lt;/code&gt;, &lt;code&gt;udev&lt;/code&gt;, &lt;code&gt;init.d&lt;/code&gt;, the U-Boot images or &lt;code&gt;ld.so.preload&lt;/code&gt;. Debloat, from then on, was done with &lt;strong&gt;0-byte placeholders&lt;/strong&gt; inside the &lt;code&gt;KoboRoot.tgz&lt;/code&gt;, never &lt;code&gt;rm&lt;/code&gt; on the live system.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;expand&quot;&gt;&lt;p class=&quot;callout-label&quot;&gt;Expand&lt;/p&gt;&lt;p&gt;&lt;strong&gt;When the device no longer boots: RAM recovery over fastboot.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;After one of those bricks we had to build a lifeline that didn&apos;t require opening the device. The Kobo, like many i.MX6 parts, exposes a &lt;strong&gt;fastboot&lt;/strong&gt; mode that enumerates as USB &lt;code&gt;18d1:0d02&lt;/code&gt; (&quot;USB download gadget&quot;), but its &lt;code&gt;boot&lt;/code&gt; command calls NXP&apos;s &lt;code&gt;boota&lt;/code&gt; routine and &lt;strong&gt;expects an Android &lt;code&gt;boot.img&lt;/code&gt; v0&lt;/strong&gt;, not a classic uImage. With &lt;code&gt;mkbootimg&lt;/code&gt; (kernel at &lt;code&gt;0x80008000&lt;/code&gt;, ramdisk at &lt;code&gt;0x83800000&lt;/code&gt;, DTB as &lt;em&gt;second&lt;/em&gt; at &lt;code&gt;0x83000000&lt;/code&gt;) the device enumerates as &quot;KoboRescue&quot; in four seconds and boots a rescue system from RAM, without touching the eMMC.&lt;/p&gt;&lt;p&gt;The detail that turned the rescue tool into a dangerous one: disassembling U-Boot we found that &lt;code&gt;_detect_bootmode&lt;/code&gt; runs inside &lt;code&gt;bootz&lt;/code&gt;, and that holding &lt;em&gt;power&lt;/em&gt; + &lt;em&gt;page&lt;/em&gt; for a couple of seconds after entering fastboot triggers &quot;recovery cus 9&quot; → &lt;code&gt;root=/dev/mmcblk0p2&lt;/code&gt; → the recovery &lt;code&gt;rcS&lt;/code&gt; reflashes 4.33&apos;s U-Boot and kernel &lt;strong&gt;and formats p1 and the onboard, without asking&lt;/strong&gt;. The MBR counter doesn&apos;t intervene here. The tool was fixed to claim the interface immediately and send &lt;code&gt;getvar:version&lt;/code&gt;, disarming the one-second timeout that triggered that path.&lt;/p&gt;&lt;/aside&gt;
&lt;h3 id=&quot;kernel-and-modules-trust-rebuilt-from-the-binary&quot;&gt;Kernel and modules: trust rebuilt from the binary&lt;/h3&gt;
&lt;p&gt;Wanting to load our own modules (zram, tun) into Kobo&apos;s kernel forces you to rebuild the trust Kobo doesn&apos;t publish. The kernel verifies &lt;code&gt;vermagic&lt;/code&gt; and the per-symbol &lt;strong&gt;CRCs&lt;/strong&gt;; a module built against headers that don&apos;t match to the bit is rejected with &lt;code&gt;Invalid argument&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The process was binary archaeology:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extract the device&apos;s own &lt;code&gt;config.gz&lt;/code&gt; (via &lt;code&gt;IKCONFIG_PROC&lt;/code&gt;) and its &lt;code&gt;kallsyms&lt;/code&gt; table: &lt;strong&gt;75,337 symbols&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Rebuild the stock kernel&apos;s &lt;code&gt;Module.symvers&lt;/code&gt; straight from the official zImage, with an &lt;strong&gt;own LZO1X decompressor&lt;/strong&gt; to open it and locating &lt;code&gt;__ksymtab&lt;/code&gt;/&lt;code&gt;__kcrctab&lt;/code&gt; with kallsyms: &lt;strong&gt;6,847 symbols&lt;/strong&gt;, all cross-checked against &lt;code&gt;kallsyms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify each module with &lt;code&gt;check-module.py&lt;/code&gt; (&lt;code&gt;vermagic&lt;/code&gt; identity) and &lt;code&gt;verify-crcs.sh&lt;/code&gt;, recomputing the CRCs from our own sources: &lt;strong&gt;186 equal, 0 different&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Libra 2 (2021) kernel tree didn&apos;t carry this board&apos;s drivers, but the &lt;strong&gt;Clara 2E&lt;/strong&gt; (2023) one matched the device config except for one option. The builds came out &lt;strong&gt;reproducible&lt;/strong&gt;: &lt;code&gt;tun.ko&lt;/code&gt; and &lt;code&gt;zsmalloc.ko&lt;/code&gt; byte-for-byte identical across builds. That&apos;s the difference between &quot;seems to work&quot; and &quot;I know exactly what I&apos;m loading into ring 0.&quot;&lt;/p&gt;
&lt;h2 id=&quot;what-plato-is-and-what-platox-is&quot;&gt;What Plato is, and what PlatoX is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Plato&lt;/strong&gt; is an open-source document reader for e-readers, written in &lt;strong&gt;Rust&lt;/strong&gt; by Bastien Dejean, meant as an alternative to Nickel: fast, no store, no account. It is the reader; it is not the system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PlatoX&lt;/strong&gt; is the idea that gives meaning to everything above: take Plato and turn it into the device&apos;s &lt;strong&gt;whole operating system&lt;/strong&gt;. Instead of patching Nickel, we replace it entirely. The device boots a small &lt;strong&gt;Buildroot system from RAM&lt;/strong&gt;, verified by sha256, and lands straight in Plato: no store, no account, no sync service. Technically, PlatoX is &lt;strong&gt;Plato 0.9.45 plus a nine-patch series&lt;/strong&gt; (&lt;code&gt;plato-patches/0001–0009&lt;/code&gt;) and a handful of own Rust &lt;em&gt;crates&lt;/em&gt;. Nickel stays installed and untouched: if anything fails, the Kobo boots Nickel as always.&lt;/p&gt;
&lt;p&gt;It&apos;s worth fixing the two names before we go on, because from here it matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KoboX&lt;/strong&gt; (and its evolution KoboX 2 / &quot;0.2 Master Edition&quot;) is the &lt;strong&gt;full personal firmware&lt;/strong&gt;, with things that depend on private infrastructure: Tailscale, a terminal, markets, weather, news, Home Assistant, library sync.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlatoX 0.1&lt;/strong&gt; is the &lt;strong&gt;public distillation&lt;/strong&gt; (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.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;10 2 1353.3 554&quot; width=&quot;1353&quot; height=&quot;554&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-platox-arquitectura-en-t fig-platox-arquitectura-en-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 1353px&quot;&gt;&lt;title id=&quot;fig-platox-arquitectura-en-t&quot;&gt;PlatoX layered architecture: platox-boot verifies sha256 and boots Buildroot in RAM, lands in Plato 0.9.45 with patches, which calls the Rust crates platox-core, platox-ai, platox-calc and platox-sys; the AI crate talks to the LLM provider over HTTPS; Nickel stays as a fallback&lt;/title&gt;&lt;desc id=&quot;fig-platox-arquitectura-en-d&quot;&gt;platox-boot (C) verifies sha256 Buildroot in RAM → Plato 0.9.45 + 9 patches library · reader · launcher · UI; platox-boot (C) verifies sha256 Buildroot in RAM → Nickel untouched, as fallback (if anything fails); Plato 0.9.45 + 9 patches library · reader · launcher · UI → platox-core Wi-Fi · settings · sysinfo · cache; Plato 0.9.45 + 9 patches library · reader · launcher · UI → platox-ai prompts · streaming · offline cache; Plato 0.9.45 + 9 patches library · reader · launcher · UI → platox-calc exact decimal · keypad; platox-boot (C) verifies sha256 Buildroot in RAM → platox-sys platox binary: hw · splash · rtc; platox-ai prompts · streaming · offline cache → LLM provider (your key) OpenAI · Claude · DeepSeek or a server on your network (HTTPS · only on request)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-platox-arquitectura-en-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-platox-arquitectura-en-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;20&quot; y=&quot;269&quot; width=&quot;1333.3&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M502.1,65.7 L796.8,122.5 Q804.7,124 804.7,132 L804.7,162&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M371.1,86 L331.8,118.9 Q325.7,124 325.7,132 L325.7,162&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;243.4&quot; y=&quot;113&quot; width=&quot;164.7&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;325.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;325.7&quot; y=&quot;124&quot;&gt;if anything fails&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M964.3,213.9 L1164.7,243.8 Q1172.6,245 1172.6,253 L1172.6,264 Q1172.6,272 1172.6,280 L1172.6,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M804.7,218 L804.7,237 Q804.7,245 804.7,253 L804.7,264 Q804.7,272 804.7,280 L804.7,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M645.1,218 L499.2,243.6 Q491.3,245 491.3,253 L491.3,264 Q491.3,272 491.3,280 L491.3,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M328.5,78.1 L199.2,121.5 Q191.6,124 191.6,132 L191.6,182 Q191.6,190 191.6,198 L191.6,237 Q191.6,245 191.6,253 L191.6,264 Q191.6,272 191.6,280 L191.6,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M804.7,355 L804.7,374 Q804.7,382 804.7,390 L804.7,412 Q804.7,420 804.7,428 L804.7,458&quot; marker-end=&quot;url(#fig-platox-arquitectura-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;695&quot; y=&quot;409&quot; width=&quot;219.3&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;804.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;420&quot;&gt;HTTPS · only on request&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;26&quot; y=&quot;275&quot; width=&quot;95.1&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;31&quot; y=&quot;283&quot; dominant-baseline=&quot;central&quot;&gt;Rust crates&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;328.5&quot; y=&quot;12&quot; width=&quot;173.6&quot; height=&quot;74&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;415.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;415.3&quot; y=&quot;31&quot;&gt;platox-boot (C)&lt;/tspan&gt;&lt;tspan x=&quot;415.3&quot; y=&quot;49&quot;&gt;verifies sha256&lt;/tspan&gt;&lt;tspan x=&quot;415.3&quot; y=&quot;67&quot;&gt;Buildroot in RAM&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;211.6&quot; y=&quot;162&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;325.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;325.7&quot; y=&quot;181&quot;&gt;Nickel&lt;/tspan&gt;&lt;tspan x=&quot;325.7&quot; y=&quot;199&quot;&gt;untouched, as fallback&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;645.1&quot; y=&quot;162&quot; width=&quot;319.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;804.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;181&quot;&gt;Plato 0.9.45 + 9 patches&lt;/tspan&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;199&quot;&gt;library · reader · launcher · UI&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;1003.9&quot; y=&quot;299&quot; width=&quot;337.4&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;1172.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;1172.6&quot; y=&quot;318&quot;&gt;platox-core&lt;/tspan&gt;&lt;tspan x=&quot;1172.6&quot; y=&quot;336&quot;&gt;Wi-Fi · settings · sysinfo · cache&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;631.4&quot; y=&quot;299&quot; width=&quot;346.5&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;804.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;318&quot;&gt;platox-ai&lt;/tspan&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;336&quot;&gt;prompts · streaming · offline cache&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;377.2&quot; y=&quot;299&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;491.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;491.3&quot; y=&quot;318&quot;&gt;platox-calc&lt;/tspan&gt;&lt;tspan x=&quot;491.3&quot; y=&quot;336&quot;&gt;exact decimal · keypad&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;32&quot; y=&quot;299&quot; width=&quot;319.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;191.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;191.6&quot; y=&quot;318&quot;&gt;platox-sys&lt;/tspan&gt;&lt;tspan x=&quot;191.6&quot; y=&quot;336&quot;&gt;platox binary: hw · splash · rtc&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;g&gt;&lt;path class=&quot;fig-node&quot; d=&quot;M667.8,464 A136.9,6 0 0 1 941.5,464 V540 A136.9,6 0 0 1 667.8,540 Z&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-node-line&quot; d=&quot;M667.8,464 A136.9,6 0 0 0 941.5,464&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;text class=&quot;fig-text&quot; x=&quot;804.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;488&quot;&gt;LLM provider (your key)&lt;/tspan&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;506&quot;&gt;OpenAI · Claude · DeepSeek&lt;/tspan&gt;&lt;tspan x=&quot;804.7&quot; y=&quot;524&quot;&gt;or a server on your network&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;Three layers: verified boot, Plato as the UI, and the Rust crates behind it&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;the-new-features-on-plato&quot;&gt;The new features on Plato&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3 id=&quot;in-public-platox-open-source&quot;&gt;In public PlatoX (open source)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plato as the launcher&lt;/strong&gt; (patch 0001): paths from the launcher, inputs by name. This is what lets Plato &lt;em&gt;be&lt;/em&gt; the system rather than one more app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network panel and Wi-Fi picker&lt;/strong&gt; (patch 0002): a new &lt;code&gt;view/network_panel.rs&lt;/code&gt; (+281 lines) that opens, from any view, a panel with Wi-Fi status in readable sentences (&lt;code&gt;Off / Looking for a known network… / Connected to X / No known network nearby&lt;/code&gt;), IP and signal. The top-bar icon has four states (&lt;code&gt;wifi-off&lt;/code&gt;, &lt;code&gt;wifi-connecting&lt;/code&gt;, &lt;code&gt;wifi&lt;/code&gt;, &lt;code&gt;wifi-secure&lt;/code&gt;) and a &lt;em&gt;poller&lt;/em&gt; thread updates it every 2 s, never from the UI thread.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI assistant&lt;/strong&gt; (patches 0004 and 0009): on a text selection, &lt;strong&gt;Summarize, Translate or Explain&lt;/strong&gt;; on a book, &lt;em&gt;insights&lt;/em&gt; —a short review, an extended review, a summary or a navigable &lt;strong&gt;concept map&lt;/strong&gt; (&lt;code&gt;view/concept_map.rs&lt;/code&gt;, +801 lines). It works &lt;strong&gt;with your own key&lt;/strong&gt;: OpenAI, &lt;strong&gt;Claude&lt;/strong&gt; (via the native Messages API with SSE &lt;em&gt;streaming&lt;/em&gt;, with &lt;code&gt;claude-opus-5-5&lt;/code&gt; preselected when your key lists it), DeepSeek or any compatible server on your network. Answers are &lt;strong&gt;cached offline&lt;/strong&gt;, and Wi-Fi only comes on when a request needs it, turning off after five idle minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phone-style calculator&lt;/strong&gt; (patch 0006): replaces Plato&apos;s old command-line calculator with a keypad one. Behind it is the &lt;code&gt;platox-calc&lt;/code&gt; crate, with an &lt;strong&gt;exact decimal&lt;/strong&gt; (&lt;code&gt;0.1 + 0.2 = 0.3&lt;/code&gt;, not &lt;code&gt;0.30000000000000004&lt;/code&gt;), percent and discount (&lt;code&gt;120 − 15 % = 102&lt;/code&gt;, with a &quot;Discount: 18&quot; line), square root and guarded division by zero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean menu, About and System Info&lt;/strong&gt; (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 &lt;strong&gt;fixes a bug in Plato itself&lt;/strong&gt;, which showed the load average multiplied by 100 with a &quot;%&quot; sign, and free memory instead of available memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer tools&lt;/strong&gt; (patch 0007): &lt;em&gt;Test All&lt;/em&gt; and &lt;em&gt;Benchmark&lt;/em&gt;, behind a developer marker, with &lt;strong&gt;secret redaction&lt;/strong&gt; in the diagnostics (no api-keys, tokens or PSKs in the logs).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;in-kobox-personal-firmware-outside-the-public-version&quot;&gt;In KoboX (personal firmware, outside the public version)&lt;/h3&gt;
&lt;p&gt;This exists and works, but depends on private infrastructure and was removed from the public build to shrink the surface: &lt;strong&gt;Tailscale&lt;/strong&gt; (first in userspace mode, then with TUN and an own module), a &lt;strong&gt;terminal&lt;/strong&gt; (busybox shell over a pty, with &lt;code&gt;fastfetch&lt;/code&gt; and &lt;code&gt;ssh&lt;/code&gt;), &lt;strong&gt;Markets&lt;/strong&gt; (Yahoo data), &lt;strong&gt;Weather&lt;/strong&gt; (Open-Meteo), &lt;strong&gt;News&lt;/strong&gt; (an own generator with &lt;code&gt;trafilatura&lt;/code&gt; in an SSRF-hardened container), &lt;strong&gt;Home Assistant&lt;/strong&gt; (lights and climate through a CONNECT tunnel) and &lt;strong&gt;PDF sync&lt;/strong&gt; against an own Calibre-Web.&lt;/p&gt;
&lt;h2 id=&quot;the-final-optimization-faster-more-robust-more-responsive-more-up-to-date&quot;&gt;The final optimization: faster, more robust, more responsive, more up to date&lt;/h2&gt;
&lt;p&gt;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: &lt;strong&gt;measure on the device, not on the host&lt;/strong&gt;. These are the medians, iteration by iteration, of version 1.4:&lt;/p&gt;



































&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric (median)&lt;/th&gt;&lt;th&gt;Before (beta7)&lt;/th&gt;&lt;th&gt;After (beta12)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Page turn (EPUB)&lt;/td&gt;&lt;td&gt;231 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;31 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Chapter change&lt;/td&gt;&lt;td&gt;2967 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;46 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Open / reopen book&lt;/td&gt;&lt;td&gt;1265 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;74 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;First &lt;em&gt;paint&lt;/em&gt; after power-on&lt;/td&gt;&lt;td&gt;9.4 s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;6.9 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Wi-Fi connected&lt;/td&gt;&lt;td&gt;12–13 s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;5.0 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--chart&quot; viewBox=&quot;0 0 864 360&quot; width=&quot;864&quot; height=&quot;360&quot; role=&quot;img&quot; aria-labelledby=&quot;chart-platox-fluidez-t chart-platox-fluidez-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 864px&quot;&gt;&lt;title id=&quot;chart-platox-fluidez-t&quot;&gt;Bar chart of chapter change in milliseconds from beta7 to beta12: 2967, 2816, 834, 260, 113 and 46 ms, with the big drop when the on-disk layout cache is introduced&lt;/title&gt;&lt;desc id=&quot;chart-platox-fluidez-d&quot;&gt;beta7: 2,967; beta8: 2,816; beta9: 834; beta10: 260; beta11: 113; beta12: 46&lt;/desc&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,316 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;316&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;0&lt;/text&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,220.7 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;220.7&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;1,000&lt;/text&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,125.3 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;125.3&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;2,000&lt;/text&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,30 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;30&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;3,000&lt;/text&gt;&lt;path class=&quot;fig-axis&quot; d=&quot;M68.5,316 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;10&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot; transform=&quot;rotate(-90 10 173)&quot;&gt;Chapter change (ms, median)&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;107&quot; y=&quot;33.1&quot; width=&quot;54&quot; height=&quot;282.9&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;134&quot; y=&quot;24.1&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;2967&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;134&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta7&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;237.9&quot; y=&quot;47.5&quot; width=&quot;54&quot; height=&quot;268.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;264.9&quot; y=&quot;38.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;2816&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;264.9&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta8&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;368.8&quot; y=&quot;236.5&quot; width=&quot;54&quot; height=&quot;79.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;395.8&quot; y=&quot;227.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;834&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;395.8&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta9&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;499.7&quot; y=&quot;291.2&quot; width=&quot;54&quot; height=&quot;24.8&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;526.7&quot; y=&quot;282.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;260&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;526.7&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta10&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;630.6&quot; y=&quot;305.2&quot; width=&quot;54&quot; height=&quot;10.8&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;657.6&quot; y=&quot;296.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;113&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;657.6&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta11&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--highlight&quot; x=&quot;761.5&quot; y=&quot;311.6&quot; width=&quot;54&quot; height=&quot;4.4&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;788.5&quot; y=&quot;302.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;46&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;788.5&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta12&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;461.3&quot; y=&quot;352&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;Iteration (measured on the Kobo)&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;461.3&quot; y=&quot;53.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;on-disk layout cache + next-chapter worker: 2967 → 46 ms&lt;/text&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;Chapter change, from 2967 to 46 ms: where it dropped and why&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id=&quot;faster&quot;&gt;Faster&lt;/h3&gt;
&lt;p&gt;The work (patch 0003) attacks every layer of the cost:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page turn:&lt;/strong&gt; a direct gray8-to-RGB565 copy with a 256-entry table (&lt;code&gt;framebuffer/fast16.rs&lt;/code&gt;) and the &lt;em&gt;prerender&lt;/em&gt; of the neighbour page on a separate thread (nice 5). beta8&apos;s page turn fell from 230.7 to 42.8 ms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text and layout:&lt;/strong&gt; per-word &lt;em&gt;shaping&lt;/em&gt; cache, reused HarfBuzz buffers, hyphenation patterns loaded lazily per language, CSS parsed once per document.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chapters without waits:&lt;/strong&gt; next-chapter layout in a &lt;em&gt;worker&lt;/em&gt; and an &lt;strong&gt;on-disk layout cache&lt;/strong&gt; (&lt;code&gt;prefetch.rs&lt;/code&gt;, &lt;code&gt;layoutcache.rs&lt;/code&gt;, &lt;code&gt;dlcodec.rs&lt;/code&gt;), verified by dev/inode/size/mtime. That&apos;s the bar that collapses in the chart.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compilation:&lt;/strong&gt; MuPDF, HarfBuzz, FreeType and DjVuLibre built from source with &lt;strong&gt;LTO and NEON&lt;/strong&gt; (layout dropped from 13.4 to &lt;strong&gt;9.8 ms/KB&lt;/strong&gt;), and the release profile with &lt;code&gt;lto&lt;/code&gt; &lt;em&gt;fat&lt;/em&gt;, &lt;code&gt;codegen-units = 1&lt;/code&gt; and &lt;code&gt;target-cpu=cortex-a9&lt;/code&gt;. The binary shrank from 10,494,280 to 8,851,200 bytes (−16%).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E-ink:&lt;/strong&gt; partial refresh between pages and a full refresh every 20 to control &lt;em&gt;ghosting&lt;/em&gt;, plus &lt;code&gt;zram&lt;/code&gt; to stretch the 512 MB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;more-responsive&quot;&gt;More responsive&lt;/h3&gt;
&lt;p&gt;Responsiveness isn&apos;t only the UI. Wi-Fi took 10 seconds to scan because of a fixed timeout in the &lt;code&gt;wext&lt;/code&gt; driver; switching it to &lt;code&gt;nl80211,wext&lt;/code&gt; dropped the scan from 10.5 to &lt;strong&gt;1.4 s&lt;/strong&gt; and auto-connect to &lt;strong&gt;5.0 s&lt;/strong&gt;. Shutdown and restart were measured at &lt;strong&gt;0.18–0.23 s&lt;/strong&gt; between the request and the clean unmount of the onboard.&lt;/p&gt;
&lt;h3 id=&quot;more-robust&quot;&gt;More robust&lt;/h3&gt;
&lt;p&gt;Robustness, in this project, is a security property. The heart is &lt;code&gt;platox-boot&lt;/code&gt;, a &lt;strong&gt;static C&lt;/strong&gt; program installed as PID 1 on Kobo&apos;s system partition. On every boot it verifies the kernel, a &lt;code&gt;boot.manifest&lt;/code&gt; and the &lt;strong&gt;sha256&lt;/strong&gt; of the system image; and if anything fails —or if &lt;strong&gt;three boots in a row never reach Plato&lt;/strong&gt;— it hands control to the stock init, Nickel. Plato&apos;s first &lt;em&gt;paint&lt;/em&gt; resets that counter.&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;2 2 606.4 810.8&quot; width=&quot;606&quot; height=&quot;811&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-platox-boot-chain-en-t fig-platox-boot-chain-en-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 606px&quot;&gt;&lt;title id=&quot;fig-platox-boot-chain-en-t&quot;&gt;platox-boot decision diagram: it checks a known kernel, a valid manifest and sha256, and fewer than three failed attempts; if all pass it unpacks the system into RAM and boots PlatoX, and any failure falls to Nickel&lt;/title&gt;&lt;desc id=&quot;fig-platox-boot-chain-en-d&quot;&gt;/sbin/init = platox-boot static C, on Kobo&apos;s partition → Known kernel and .platox/enabled present?; Known kernel and .platox/enabled present? → boot.manifest: size and sha256 match? (yes); boot.manifest: size and sha256 match? → Fewer than 3 boots that never reached Plato? (yes); Fewer than 3 boots that never reached Plato? → System into RAM tmpfs + pivot_root (yes); System into RAM tmpfs + pivot_root → PlatoX Plato 0.9.45 + patches; Known kernel and .platox/enabled present? → stock init: Nickel the safety net (no); boot.manifest: size and sha256 match? → stock init: Nickel the safety net (no); Fewer than 3 boots that never reached Plato? → stock init: Nickel the safety net (no: 3 failures)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-platox-boot-chain-en-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-platox-boot-chain-en-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M407.6,68 L407.6,87 Q407.6,95 407.6,103 L407.6,122&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M354.9,217.6 L318.8,250.2 Q312.9,255.6 312.9,263.6 L312.9,293.6&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;294.3&quot; y=&quot;244.6&quot; width=&quot;37.3&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;312.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;312.9&quot; y=&quot;255.6&quot;&gt;yes&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M250.7,389.2 L207.6,422.3 Q201.2,427.2 201.2,435.2 L201.2,465.2&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;182.6&quot; y=&quot;416.2&quot; width=&quot;37.3&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;201.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;201.2&quot; y=&quot;427.2&quot;&gt;yes&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M173,560.8 L154.6,591.9 Q150.6,598.8 150.6,606.8 L150.6,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;131.9&quot; y=&quot;587.8&quot; width=&quot;37.3&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;150.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.6&quot; y=&quot;598.8&quot;&gt;yes&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M150.6,692.8 L150.6,711.8 Q150.6,719.8 150.6,727.8 L150.6,746.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M477.7,217.6 L526.7,251.1 Q533.3,255.6 533.3,263.6 L533.3,333.4 Q533.3,341.4 533.3,349.4 L533.3,419.2 Q533.3,427.2 533.3,435.2 L533.3,505 Q533.3,513 533.3,521 L533.3,590.8 Q533.3,598.8 530,606 L515.6,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;519.2&quot; y=&quot;416.2&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;533.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;533.3&quot; y=&quot;427.2&quot;&gt;no&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M375.1,389.2 L418.2,422.3 Q424.6,427.2 424.6,435.2 L424.6,505 Q424.6,513 424.6,521 L424.6,590.8 Q424.6,598.8 430.7,604 L469.4,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;410.5&quot; y=&quot;502&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;424.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;424.6&quot; y=&quot;513&quot;&gt;no&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M240.4,560.8 L266.5,592.6 Q271.6,598.8 279.2,601 L406.6,637.4&quot; marker-end=&quot;url(#fig-platox-boot-chain-en-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;202.9&quot; y=&quot;587.8&quot; width=&quot;137.4&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;271.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;271.6&quot; y=&quot;598.8&quot;&gt;no: 3 failures&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;261.7&quot; y=&quot;12&quot; width=&quot;291.9&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;407.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;31&quot;&gt;/sbin/init = platox-boot&lt;/tspan&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;49&quot;&gt;static C, on Kobo&apos;s partition&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;407.6,122 590.3,169.8 407.6,217.6 225,169.8&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;407.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;160.8&quot;&gt;Known kernel and&lt;/tspan&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;178.8&quot;&gt;.platox/enabled present?&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;312.9,293.6 482.4,341.4 312.9,389.2 143.5,341.4&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;312.9&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;312.9&quot; y=&quot;332.4&quot;&gt;boot.manifest:&lt;/tspan&gt;&lt;tspan x=&quot;312.9&quot; y=&quot;350.4&quot;&gt;size and sha256 match?&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;201.2,465.2 390.5,513 201.2,560.8 12,513&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;201.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;201.2&quot; y=&quot;504&quot;&gt;Fewer than 3 boots&lt;/tspan&gt;&lt;tspan x=&quot;201.2&quot; y=&quot;522&quot;&gt;that never reached Plato?&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;54.7&quot; y=&quot;636.8&quot; width=&quot;191.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;150.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.6&quot; y=&quot;655.8&quot;&gt;System into RAM&lt;/tspan&gt;&lt;tspan x=&quot;150.6&quot; y=&quot;673.8&quot;&gt;tmpfs + pivot_root&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;36.5&quot; y=&quot;746.8&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;150.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.6&quot; y=&quot;765.8&quot;&gt;PlatoX&lt;/tspan&gt;&lt;tspan x=&quot;150.6&quot; y=&quot;783.8&quot;&gt;Plato 0.9.45 + patches&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;406.6&quot; y=&quot;636.8&quot; width=&quot;191.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;502.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;502.5&quot; y=&quot;655.8&quot;&gt;stock init: Nickel&lt;/tspan&gt;&lt;tspan x=&quot;502.5&quot; y=&quot;673.8&quot;&gt;the safety net&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;The boot selector: three checks and a safety net&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The kernel-slot writer is just as careful: it writes with &lt;code&gt;dd conv=notrunc,fsync&lt;/code&gt;, reads back and checks the sha256, and if it doesn&apos;t match it &lt;strong&gt;restores the verified previous copy&lt;/strong&gt; without rebooting, all under &lt;code&gt;timeout 300&lt;/code&gt; and only with the battery at 30% or charging. KoboX 1.4&apos;s own init goes further and &lt;strong&gt;rejects foreign update packages&lt;/strong&gt;: it applies only its own, with a manifest (sha256, size, paths, space, battery), and sets any stray &lt;code&gt;KoboRoot.tgz&lt;/code&gt; aside without running it. The stable version (Master Edition) rounded this off with atomic config writes and a clean &lt;code&gt;clippy -D warnings&lt;/code&gt; across all eight crates.&lt;/p&gt;
&lt;h3 id=&quot;more-up-to-date&quot;&gt;More up to date&lt;/h3&gt;
&lt;p&gt;Where the stock system was a 2015 museum, the own userland is today&apos;s: &lt;strong&gt;Buildroot 2025.02 LTS&lt;/strong&gt;, glibc 2.41, busybox 1.37, &lt;code&gt;wpa_supplicant 2.12&lt;/code&gt; with &lt;strong&gt;mbedTLS&lt;/strong&gt; and &lt;strong&gt;WPA3/SAE/OWE&lt;/strong&gt; support — and &lt;strong&gt;no OpenSSL, no udev and no systemd&lt;/strong&gt;. There&apos;s even an own &lt;strong&gt;4.1.15&lt;/strong&gt; kernel rebuilt from Kobo&apos;s GPL sources with reliable suspend: the device hung on suspend, and by &lt;strong&gt;config bisection&lt;/strong&gt; we found the cause was, paradoxically, &lt;em&gt;removing&lt;/em&gt; &lt;code&gt;CONFIG_PM_DEBUG&lt;/code&gt;; those debug messages were hiding a &lt;em&gt;race&lt;/em&gt; in the board&apos;s suspend, so the fix was to leave it as in stock.&lt;/p&gt;
&lt;h2 id=&quot;method-note-and-threat-model&quot;&gt;Method note and threat model&lt;/h2&gt;
&lt;p&gt;What this is and what it isn&apos;t, stated plainly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;There&apos;s integrity, not authenticity.&lt;/strong&gt; &lt;code&gt;platox-boot&lt;/code&gt; verifies the system&apos;s sha256 on every boot, but &lt;strong&gt;there are no signatures&lt;/strong&gt;. A local attacker who can write to the onboard partition can replace the &lt;code&gt;cpio&lt;/code&gt; and its manifest at once. This design&apos;s threat model is &lt;strong&gt;anti-brick and anti-corruption&lt;/strong&gt;, not anti-attacker with physical access and write permission. Closing that would require the one thing we didn&apos;t touch: U-Boot and a root of trust in the SoC.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The API key is stored in plain text&lt;/strong&gt;, in &lt;code&gt;.platox/config/ai.toml&lt;/code&gt;, on the Kobo&apos;s own storage. It&apos;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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It doesn&apos;t phone home.&lt;/strong&gt; No telemetry, no account, no update check. The device talks to the AI provider you configure, only when you ask it to, and to &lt;code&gt;pool.ntp.org&lt;/code&gt; for the time. Wi-Fi is off unless an action needs it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debloat doesn&apos;t delete, it hides.&lt;/strong&gt; It&apos;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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supply chain:&lt;/strong&gt; sources pinned by version and sha256, reproducible kernel and packages, and a &lt;code&gt;check-public.sh&lt;/code&gt; that refuses personal data, private infrastructure and Kobo&apos;s proprietary files before every release.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-we-take-away&quot;&gt;What we take away&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The updater is the vector.&lt;/strong&gt; 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&apos;s where the signature goes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State on disk is security state.&lt;/strong&gt; Five bytes in the MBR decided whether the device wiped itself. Whoever replaces the system inherits its invariants; discovering them late costs a device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrity isn&apos;t authenticity.&lt;/strong&gt; 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&apos;re solving is half the work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bricks are data.&lt;/strong&gt; Every device we left unusable —permissions, space, one too many &lt;code&gt;pickel-imx&lt;/code&gt; on a delete list— taught a system invariant no documentation told us.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plato&lt;/strong&gt;, by Bastien Dejean — the open-source reader at the heart of it all: &lt;a href=&quot;https://github.com/baskerville/plato&quot;&gt;github.com/baskerville/plato&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlatoX&lt;/strong&gt; — the public edition (AGPL-3.0) of this work; no public repository yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buildroot&lt;/strong&gt; — the embedded system that boots in RAM: &lt;a href=&quot;https://buildroot.org&quot;&gt;buildroot.org&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>sec</category></item><item><title>Convertir un e-reader en tu sistema: ingeniería inversa del Kobo Libra 2</title><link>https://quantarmy.com/ingenieria-inversa-kobo-libra-2-plato/</link><guid isPermaLink="true">https://quantarmy.com/ingenieria-inversa-kobo-libra-2-plato/</guid><description>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.</description><pubDate>Wed, 04 Nov 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;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 &quot;no tiene nada dentro&quot;. Y sin embargo tiene todo lo que hace interesante a un objetivo de hardware: una cadena de arranque &lt;strong&gt;sin secure boot&lt;/strong&gt;, 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.&lt;/p&gt;
&lt;p&gt;Este artículo es el registro de lo que le hicimos a un &lt;strong&gt;Kobo Libra 2&lt;/strong&gt;: 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 &lt;strong&gt;Plato&lt;/strong&gt; —un lector de documentos open-source— en el sistema operativo entero del dispositivo. El resultado público se llama &lt;strong&gt;PlatoX&lt;/strong&gt; y es software libre (AGPL-3.0); la versión personal, con más cosas enganchadas, se llamó &lt;strong&gt;KoboX&lt;/strong&gt;. Vamos a separar las dos con cuidado, porque la seguridad está en los detalles y mezclar ediciones es mentir con precisión.&lt;/p&gt;
&lt;p&gt;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é &quot;integridad&quot; y &quot;autenticidad&quot; no son la misma palabra.&lt;/p&gt;
&lt;h2 id=&quot;el-ecosistema-que-había-que-entender-antes-de-tocar-nada&quot;&gt;El ecosistema que había que entender antes de tocar nada&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;El aparato es un &lt;strong&gt;Kobo Libra 2&lt;/strong&gt;, nombre interno &lt;code&gt;io&lt;/code&gt;, placa &lt;strong&gt;E70K10&lt;/strong&gt;, confirmado leyendo la tabla de PCB de &lt;code&gt;kobo_config.sh&lt;/code&gt; del propio firmware. Dentro: un SoC &lt;strong&gt;NXP i.MX6SLL&lt;/strong&gt; (ARM Cortex-A9), PMIC Rohm &lt;strong&gt;BD71828&lt;/strong&gt;, Wi-Fi &lt;strong&gt;Realtek RTL8723DS&lt;/strong&gt; por SDIO, pantalla e-ink de &lt;strong&gt;1264×1680&lt;/strong&gt; y &lt;strong&gt;512 MB&lt;/strong&gt; 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:&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;10 2 927.6 595&quot; width=&quot;928&quot; height=&quot;595&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-kobo-ecosistema-es-t fig-kobo-ecosistema-es-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 928px&quot;&gt;&lt;title id=&quot;fig-kobo-ecosistema-es-t&quot;&gt;Cadena de arranque de serie del Kobo Libra 2: hardware i.MX6SLL, U-Boot sin secure boot, kernel Linux 4.1.15, busybox init y Nickel como lector; aparte, los servicios de serie pickel, udev y wpa_supplicant&lt;/title&gt;&lt;desc id=&quot;fig-kobo-ecosistema-es-d&quot;&gt;Kobo Libra 2 (io / E70K10) SoC NXP i.MX6SLL · Wi-Fi RTL8723DS e-ink 1264×1680 · 512 MB RAM → U-Boot NXP 2016.03-00128 sin secure boot ni firma; U-Boot NXP 2016.03-00128 sin secure boot ni firma → Kernel Linux 4.1.15 fuentes GPL de Kobo; Kernel Linux 4.1.15 fuentes GPL de Kobo → busybox init 1.31.1 /etc/init.d/rcS · glibc 2.19; busybox init 1.31.1 /etc/init.d/rcS · glibc 2.19 → Nickel lector Qt · tienda · cuenta (arranque normal); busybox init 1.31.1 /etc/init.d/rcS · glibc 2.19 → pickel factory-reset · upgrade (lanza servicios)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-kobo-ecosistema-es-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-kobo-ecosistema-es-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;20&quot; y=&quot;489&quot; width=&quot;605.9&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M470.7,86 L470.7,105 Q470.7,113 470.7,121 L470.7,140&quot; marker-end=&quot;url(#fig-kobo-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M470.7,196 L470.7,215 Q470.7,223 470.7,231 L470.7,250&quot; marker-end=&quot;url(#fig-kobo-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M470.7,306 L470.7,325 Q470.7,333 470.7,341 L470.7,360&quot; marker-end=&quot;url(#fig-kobo-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M606.5,416 L782.9,452.4 Q790.8,454 790.8,462 L790.8,484 Q790.8,492 790.8,500 L790.8,519&quot; marker-end=&quot;url(#fig-kobo-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;717.5&quot; y=&quot;443&quot; width=&quot;146.5&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;790.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;790.8&quot; y=&quot;454&quot;&gt;arranque normal&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M334.9,416 L158.5,452.4 Q150.7,454 150.7,462 L150.7,484 Q150.7,492 150.7,500 L150.7,519&quot; marker-end=&quot;url(#fig-kobo-ecosistema-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;77.4&quot; y=&quot;443&quot; width=&quot;146.5&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;150.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.7&quot; y=&quot;454&quot;&gt;lanza servicios&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;26&quot; y=&quot;495&quot; width=&quot;149.2&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;31&quot; y=&quot;503&quot; dominant-baseline=&quot;central&quot;&gt;Servicios de serie&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;302&quot; y=&quot;12&quot; width=&quot;337.4&quot; height=&quot;74&quot; rx=&quot;37&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;31&quot;&gt;Kobo Libra 2 (io / E70K10)&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;49&quot;&gt;SoC NXP i.MX6SLL · Wi-Fi RTL8723DS&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;67&quot;&gt;e-ink 1264×1680 · 512 MB RAM&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;347.5&quot; y=&quot;140&quot; width=&quot;246.4&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;159&quot;&gt;U-Boot NXP 2016.03-00128&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;177&quot;&gt;sin secure boot ni firma&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;370.3&quot; y=&quot;250&quot; width=&quot;200.9&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;269&quot;&gt;Kernel Linux 4.1.15&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;287&quot;&gt;fuentes GPL de Kobo&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;329.3&quot; y=&quot;360&quot; width=&quot;282.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;470.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;379&quot;&gt;busybox init 1.31.1&lt;/tspan&gt;&lt;tspan x=&quot;470.7&quot; y=&quot;397&quot;&gt;/etc/init.d/rcS · glibc 2.19&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;653.9&quot; y=&quot;519&quot; width=&quot;273.7&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;790.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;790.8&quot; y=&quot;538&quot;&gt;Nickel&lt;/tspan&gt;&lt;tspan x=&quot;790.8&quot; y=&quot;556&quot;&gt;lector Qt · tienda · cuenta&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;32&quot; y=&quot;519&quot; width=&quot;237.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;150.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.7&quot; y=&quot;538&quot;&gt;pickel&lt;/tspan&gt;&lt;tspan x=&quot;150.7&quot; y=&quot;556&quot;&gt;factory-reset · upgrade&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;295.3&quot; y=&quot;528&quot; width=&quot;100.8&quot; height=&quot;38&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;345.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;345.7&quot; y=&quot;547&quot;&gt;udev 142&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;422.1&quot; y=&quot;519&quot; width=&quot;191.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;518&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;518&quot; y=&quot;538&quot;&gt;wpa_supplicant 2.9&lt;/tspan&gt;&lt;tspan x=&quot;518&quot; y=&quot;556&quot;&gt;OpenSSL 1.0.1u&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;El stack de serie: de U-Boot a Nickel, sin un solo eslabón firmado&lt;/figcaption&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Arranque:&lt;/strong&gt; U-Boot de NXP (&lt;code&gt;2016.03-00128&lt;/code&gt; en la rama 4.38), &lt;strong&gt;sin verificación de firma&lt;/strong&gt;. Quien controle lo que el kernel ejecuta como init, controla el aparato.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kernel:&lt;/strong&gt; Linux &lt;strong&gt;4.1.15&lt;/strong&gt;, build exacto de Kobo &lt;code&gt;-00868-g58a2758be07&lt;/code&gt;, compilado con GCC 5.3.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Userland:&lt;/strong&gt; &lt;code&gt;busybox 1.31.1&lt;/code&gt; como PID 1, &lt;strong&gt;glibc 2.19&lt;/strong&gt;, libstdc++ de GCC 4.9. udev 142, &lt;code&gt;wpa_supplicant 2.9&lt;/code&gt; sobre &lt;strong&gt;OpenSSL 1.0.1u&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El lector:&lt;/strong&gt; &lt;strong&gt;Nickel&lt;/strong&gt;, la aplicación Qt de Kobo, con su tienda y su cuenta.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pickel&lt;/code&gt;:&lt;/strong&gt; un wrapper de shell que ejecuta &lt;code&gt;pickel-imx&lt;/code&gt; (un ELF). Guárdalo en la cabeza: &lt;code&gt;pickel&lt;/code&gt; es quien decide si el aparato hace factory-reset o acepta una actualización. Es, sin saberlo, una pieza de seguridad.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El almacenamiento es un eMMC &lt;code&gt;/dev/mmcblk0&lt;/code&gt; con tres particiones y varias zonas crudas fuera de ellas:&lt;/p&gt;








































&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Tabla&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Zona&lt;/th&gt;&lt;th&gt;Qué es&lt;/th&gt;&lt;th&gt;Tamaño / posición&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mmcblk0p1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;rootfs&lt;/code&gt; (donde corre Nickel)&lt;/td&gt;&lt;td&gt;256 MB ext4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mmcblk0p2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;recoveryfs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;384 MB ext4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mmcblk0p3&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;KOBOeReader&lt;/code&gt; (el &quot;onboard&quot;, visible por USB)&lt;/td&gt;&lt;td&gt;29,1 GB vfat&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;zImage&lt;/td&gt;&lt;td&gt;kernel crudo&lt;/td&gt;&lt;td&gt;sector 2048&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;hwconfig&lt;/td&gt;&lt;td&gt;config de hardware&lt;/td&gt;&lt;td&gt;sector 1024&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;waveform&lt;/td&gt;&lt;td&gt;tabla e-ink&lt;/td&gt;&lt;td&gt;sector 14336&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Ese &lt;code&gt;256 MB&lt;/code&gt; de la rootfs y ese &lt;code&gt;vfat&lt;/code&gt; 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 &lt;strong&gt;4.38.23828&lt;/strong&gt; (los hashes de kernel que el sistema acepta están anclados a ella).&lt;/p&gt;
&lt;h2 id=&quot;ingeniería-inversa-proceso-y-resultados&quot;&gt;Ingeniería inversa: proceso y resultados&lt;/h2&gt;
&lt;p&gt;Con el mapa en la mano, la pregunta operativa es una sola: &lt;strong&gt;¿cómo se mete código propio sin abrir el aparato ni tocar U-Boot?&lt;/strong&gt; La respuesta estaba en el propio mecanismo de actualización de Kobo.&lt;/p&gt;
&lt;h3 id=&quot;el-vector-koboroottgz-y-la-confianza-implícita&quot;&gt;El vector: &lt;code&gt;KoboRoot.tgz&lt;/code&gt; y la confianza implícita&lt;/h3&gt;
&lt;p&gt;Kobo actualiza así: si el init de serie encuentra un fichero &lt;code&gt;.kobo/KoboRoot.tgz&lt;/code&gt; en el almacenamiento, lo desempaqueta sobre &lt;code&gt;/&lt;/code&gt; como root y reinicia. En el &lt;code&gt;rcS&lt;/code&gt; de serie eso ocurre alrededor de las líneas 251–269, con &lt;code&gt;pickel can-upgrade&lt;/code&gt; 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 &lt;strong&gt;755&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Dicho en lenguaje de amenazas: &lt;strong&gt;el actualizador es el vector&lt;/strong&gt;. Cualquiera que pueda escribir en la partición de usuario —que se monta por USB sin credenciales— puede colocar un &lt;code&gt;KoboRoot.tgz&lt;/code&gt; 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.&lt;/p&gt;
&lt;h3 id=&quot;el-contador-de-arranque-en-el-mbr&quot;&gt;El contador de arranque en el MBR&lt;/h3&gt;
&lt;p&gt;El hallazgo más bonito de toda la ingeniería inversa no estaba en un binario, sino en cinco bytes del MBR. En &lt;code&gt;/dev/mmcblk0&lt;/code&gt;, offsets &lt;strong&gt;0x133–0x137&lt;/strong&gt;, Kobo guarda un &lt;strong&gt;contador de arranque&lt;/strong&gt;. En cada boot, &lt;code&gt;pickel should-factory-reset&lt;/code&gt; lo incrementa; cuando el índice llega a &lt;strong&gt;4&lt;/strong&gt;, el aparato muestra la pantalla de factory-reset y se borra. Lo único que resetea ese contador es &lt;strong&gt;Nickel&lt;/strong&gt;, que escribe la secuencia &lt;code&gt;8d ef 02 d2 00&lt;/code&gt; cuando arranca bien.&lt;/p&gt;
&lt;p&gt;La consecuencia de seguridad es profunda: &lt;strong&gt;un contador en disco es estado de seguridad&lt;/strong&gt;, 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 &lt;code&gt;8d ef 02 d2 00&lt;/code&gt; en el offset &lt;code&gt;307&lt;/code&gt;, releer y registrar— solo cuando Plato llevaba vivo más de un umbral de tiempo. Un test de mutación (escribir en el offset &lt;code&gt;308&lt;/code&gt;) debía fallar, y fallaba. Eso llegó en la versión 1.0.3, después de verlo borrarse en el banco de pruebas.&lt;/p&gt;
&lt;h3 id=&quot;los-bricks-contados-como-lo-que-son-resultados&quot;&gt;Los bricks, contados como lo que son: resultados&lt;/h3&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Permisos (&lt;code&gt;EACCES&lt;/code&gt;):&lt;/strong&gt; empaquetamos &lt;code&gt;rcS&lt;/code&gt; en modo &lt;code&gt;644&lt;/code&gt;. &lt;code&gt;busybox init&lt;/code&gt; lo invoca con &lt;code&gt;execv&lt;/code&gt;, que exige el bit de ejecución → &lt;code&gt;EACCES&lt;/code&gt; → no arranca nada. Se corrigió enviándolo en &lt;code&gt;755&lt;/code&gt; con un &lt;code&gt;check_exec&lt;/code&gt; de guarda. (fix 1.0.1)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Espacio (&lt;code&gt;ENOSPC&lt;/code&gt;):&lt;/strong&gt; metimos 44,5 MB en una rootfs de 256 MB que ya iba al 93%. El &lt;code&gt;etc/&lt;/code&gt; se extendió a medias y el aparato se quedó en el splash sin llegar a Plato. La solución fue mover Plato al &lt;strong&gt;onboard&lt;/strong&gt; (vfat) y dejar la rootfs con un delta de &lt;strong&gt;29 KB&lt;/strong&gt;, con un límite duro de ≤ 256 KB en la partición de sistema. (fix 1.0.2)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El factory-reset autoinfligido:&lt;/strong&gt; una fase de &lt;em&gt;debloat&lt;/em&gt; temprana metió todo lo de &lt;code&gt;/usr/local/Kobo/*&lt;/code&gt; en la lista de borrado… incluido &lt;code&gt;pickel-imx&lt;/code&gt;. Siguiente arranque: &lt;code&gt;pickel should-factory-reset&lt;/code&gt; devuelve 127, el sistema interpreta eso como &quot;hay que resetear&quot;, entra en recovery, reflashea el firmware &lt;strong&gt;4.33.19747&lt;/strong&gt; y &lt;strong&gt;borra el onboard&lt;/strong&gt;. De ahí nació la regla más importante del proyecto: una &lt;strong&gt;protect-list&lt;/strong&gt; que nunca deja tocar &lt;code&gt;pickel*&lt;/code&gt;, &lt;code&gt;udev&lt;/code&gt;, &lt;code&gt;init.d&lt;/code&gt;, las imágenes de U-Boot ni &lt;code&gt;ld.so.preload&lt;/code&gt;. El debloat, además, pasó a hacerse con &lt;strong&gt;placeholders de 0 bytes&lt;/strong&gt; dentro del &lt;code&gt;KoboRoot.tgz&lt;/code&gt;, nunca con &lt;code&gt;rm&lt;/code&gt; sobre el sistema vivo.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;expand&quot;&gt;&lt;p class=&quot;callout-label&quot;&gt;Expande&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Cuando el aparato ya no arranca: recuperación por RAM sobre fastboot.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Tras uno de esos bricks hubo que construir un salvavidas que no exigiera abrir el aparato. El Kobo, como muchos i.MX6, expone un modo &lt;strong&gt;fastboot&lt;/strong&gt; que enumera como USB &lt;code&gt;18d1:0d02&lt;/code&gt; (&quot;USB download gadget&quot;), pero su comando &lt;code&gt;boot&lt;/code&gt; llama a la rutina &lt;code&gt;boota&lt;/code&gt; de NXP y &lt;strong&gt;espera una imagen Android &lt;code&gt;boot.img&lt;/code&gt; v0&lt;/strong&gt;, no un uImage clásico. Con &lt;code&gt;mkbootimg&lt;/code&gt; (kernel en &lt;code&gt;0x80008000&lt;/code&gt;, ramdisk en &lt;code&gt;0x83800000&lt;/code&gt;, DTB como &lt;em&gt;second&lt;/em&gt; en &lt;code&gt;0x83000000&lt;/code&gt;) el aparato enumera como &quot;KoboRescue&quot; en cuatro segundos y arranca un sistema de rescate desde RAM, sin tocar el eMMC.&lt;/p&gt;&lt;p&gt;El detalle que convirtió la herramienta de rescate en una herramienta peligrosa: desensamblando U-Boot encontramos que &lt;code&gt;_detect_bootmode&lt;/code&gt; corre dentro de &lt;code&gt;bootz&lt;/code&gt;, y que mantener &lt;em&gt;power&lt;/em&gt; + &lt;em&gt;page&lt;/em&gt; un par de segundos tras entrar en fastboot dispara &quot;recovery cus 9&quot; → &lt;code&gt;root=/dev/mmcblk0p2&lt;/code&gt; → el &lt;code&gt;rcS&lt;/code&gt; de recovery reflashea U-Boot y kernel de la 4.33 &lt;strong&gt;y formatea la p1 y el onboard, sin preguntar&lt;/strong&gt;. El contador del MBR no interviene aquí. La herramienta se corrigió para reclamar la interfaz al instante y mandar &lt;code&gt;getvar:version&lt;/code&gt;, desarmando el timeout de un segundo que provocaba ese camino.&lt;/p&gt;&lt;/aside&gt;
&lt;h3 id=&quot;kernel-y-módulos-confianza-reconstruida-desde-el-binario&quot;&gt;Kernel y módulos: confianza reconstruida desde el binario&lt;/h3&gt;
&lt;p&gt;Querer cargar módulos propios (zram, tun) en el kernel de Kobo obliga a reconstruir la confianza que Kobo no publica. El kernel verifica &lt;code&gt;vermagic&lt;/code&gt; y los &lt;strong&gt;CRC&lt;/strong&gt; de cada símbolo; un módulo compilado contra cabeceras que no cuadran al bit se rechaza con &lt;code&gt;Invalid argument&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;El proceso fue arqueología de binarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extraer del aparato su propio &lt;code&gt;config.gz&lt;/code&gt; (vía &lt;code&gt;IKCONFIG_PROC&lt;/code&gt;) y su tabla &lt;code&gt;kallsyms&lt;/code&gt;: &lt;strong&gt;75.337 símbolos&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Reconstruir el &lt;code&gt;Module.symvers&lt;/code&gt; del kernel de serie directamente desde el zImage oficial, con un &lt;strong&gt;descompresor LZO1X propio&lt;/strong&gt; para abrirlo y localizando &lt;code&gt;__ksymtab&lt;/code&gt;/&lt;code&gt;__kcrctab&lt;/code&gt; con kallsyms: &lt;strong&gt;6.847 símbolos&lt;/strong&gt;, todos contrastados contra &lt;code&gt;kallsyms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verificar cada módulo con &lt;code&gt;check-module.py&lt;/code&gt; (identidad de &lt;code&gt;vermagic&lt;/code&gt;) y &lt;code&gt;verify-crcs.sh&lt;/code&gt;, recomputando los CRC desde fuentes propias: &lt;strong&gt;186 iguales, 0 distintos&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El árbol de kernel de la Libra 2 (2021) no traía los drivers de esta placa, pero el de la &lt;strong&gt;Clara 2E&lt;/strong&gt; (2023) encajaba con la config del aparato salvo una opción. Las compilaciones salieron &lt;strong&gt;reproducibles&lt;/strong&gt;: &lt;code&gt;tun.ko&lt;/code&gt; y &lt;code&gt;zsmalloc.ko&lt;/code&gt; idénticos byte a byte entre builds. Esa es la diferencia entre &quot;parece que funciona&quot; y &quot;sé exactamente qué estoy cargando en ring 0&quot;.&lt;/p&gt;
&lt;h2 id=&quot;qué-es-plato-y-qué-es-platox&quot;&gt;Qué es Plato, y qué es PlatoX&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Plato&lt;/strong&gt; es un lector de documentos open-source para e-readers, escrito en &lt;strong&gt;Rust&lt;/strong&gt; por Bastien Dejean, pensado como alternativa a Nickel: rápido, sin tienda, sin cuenta. Es el lector; no es el sistema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PlatoX&lt;/strong&gt; es la idea que da sentido a todo lo anterior: coger Plato y convertirlo en el &lt;strong&gt;sistema operativo entero&lt;/strong&gt; del Kobo. En vez de parchear Nickel, lo sustituimos del todo. El aparato arranca un pequeño sistema &lt;strong&gt;Buildroot desde RAM&lt;/strong&gt;, verificado por sha256, y cae directamente en Plato: sin tienda, sin cuenta, sin servicio de sincronización. Técnicamente, PlatoX es &lt;strong&gt;Plato 0.9.45 más una serie de nueve parches&lt;/strong&gt; (&lt;code&gt;plato-patches/0001–0009&lt;/code&gt;) y un puñado de &lt;em&gt;crates&lt;/em&gt; de Rust propios. Nickel se queda instalado e intacto: si algo falla, el Kobo arranca Nickel como siempre.&lt;/p&gt;
&lt;p&gt;Conviene fijar los dos nombres antes de seguir, porque de aquí en adelante importa:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KoboX&lt;/strong&gt; (y su evolución KoboX 2 / &quot;0.2 Master Edition&quot;) es el &lt;strong&gt;firmware personal completo&lt;/strong&gt;, con cosas que dependen de infraestructura propia: Tailscale, una terminal, mercados, tiempo, noticias, Home Assistant, sincronización de biblioteca.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlatoX 0.1&lt;/strong&gt; es la &lt;strong&gt;destilación pública&lt;/strong&gt; (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.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;10 2 1371.5 554&quot; width=&quot;1372&quot; height=&quot;554&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-platox-arquitectura-es-t fig-platox-arquitectura-es-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 1372px&quot;&gt;&lt;title id=&quot;fig-platox-arquitectura-es-t&quot;&gt;Arquitectura en capas de PlatoX: platox-boot verifica sha256 y arranca Buildroot en RAM, cae en Plato 0.9.45 con parches, que llama a los crates Rust platox-core, platox-ai, platox-calc y platox-sys; el crate de IA habla por HTTPS con el proveedor LLM; Nickel queda como fallback&lt;/title&gt;&lt;desc id=&quot;fig-platox-arquitectura-es-d&quot;&gt;platox-boot (C) verifica sha256 Buildroot en RAM → Plato 0.9.45 + 9 parches biblioteca · lector · launcher · UI; platox-boot (C) verifica sha256 Buildroot en RAM → Nickel intacto, como fallback (si algo falla); Plato 0.9.45 + 9 parches biblioteca · lector · launcher · UI → platox-core Wi-Fi · ajustes · sysinfo · caché; Plato 0.9.45 + 9 parches biblioteca · lector · launcher · UI → platox-ai prompts · streaming · caché offline; Plato 0.9.45 + 9 parches biblioteca · lector · launcher · UI → platox-calc decimal exacto · teclado; platox-boot (C) verifica sha256 Buildroot en RAM → platox-sys binario platox: hw · splash · rtc; platox-ai prompts · streaming · caché offline → Proveedor LLM (tu clave) OpenAI · Claude · DeepSeek o servidor en tu red (HTTPS · sólo bajo petición)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-platox-arquitectura-es-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-platox-arquitectura-es-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;g class=&quot;fig-cluster&quot;&gt;&lt;rect x=&quot;20&quot; y=&quot;269&quot; width=&quot;1351.5&quot; height=&quot;98&quot; rx=&quot;6&quot;&gt;&lt;/rect&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M511.2,65 L824.1,122.6 Q832,124 832,132 L832,162&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M378,86 L336.5,119 Q330.3,124 330.3,132 L330.3,162&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;266.1&quot; y=&quot;113&quot; width=&quot;128.3&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;330.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;330.3&quot; y=&quot;124&quot;&gt;si algo falla&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M1005.2,216.2 L1187.4,243.8 Q1195.4,245 1195.4,253 L1195.4,264 Q1195.4,272 1195.4,280 L1195.4,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M832,218 L832,237 Q832,245 832,253 L832,264 Q832,272 832,280 L832,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M667.8,218 L517.4,243.7 Q509.5,245 509.5,253 L509.5,264 Q509.5,272 509.5,280 L509.5,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M337.6,77.5 L203.8,121.5 Q196.2,124 196.2,132 L196.2,182 Q196.2,190 196.2,198 L196.2,237 Q196.2,245 196.2,253 L196.2,264 Q196.2,272 196.2,280 L196.2,299&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M832,355 L832,374 Q832,382 832,390 L832,412 Q832,420 832,428 L832,458&quot; marker-end=&quot;url(#fig-platox-arquitectura-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;708.7&quot; y=&quot;409&quot; width=&quot;246.6&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;832&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;832&quot; y=&quot;420&quot;&gt;HTTPS · sólo bajo petición&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-cluster-label-group&quot;&gt;&lt;rect class=&quot;fig-cluster-label-bg&quot; x=&quot;26&quot; y=&quot;275&quot; width=&quot;95.1&quot; height=&quot;16&quot; rx=&quot;2&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-cluster-label&quot; x=&quot;31&quot; y=&quot;283&quot; dominant-baseline=&quot;central&quot;&gt;Crates Rust&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;337.6&quot; y=&quot;12&quot; width=&quot;173.6&quot; height=&quot;74&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;424.4&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;424.4&quot; y=&quot;31&quot;&gt;platox-boot (C)&lt;/tspan&gt;&lt;tspan x=&quot;424.4&quot; y=&quot;49&quot;&gt;verifica sha256&lt;/tspan&gt;&lt;tspan x=&quot;424.4&quot; y=&quot;67&quot;&gt;Buildroot en RAM&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;216.2&quot; y=&quot;162&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;330.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;330.3&quot; y=&quot;181&quot;&gt;Nickel&lt;/tspan&gt;&lt;tspan x=&quot;330.3&quot; y=&quot;199&quot;&gt;intacto, como fallback&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;658.7&quot; y=&quot;162&quot; width=&quot;346.5&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;832&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;832&quot; y=&quot;181&quot;&gt;Plato 0.9.45 + 9 parches&lt;/tspan&gt;&lt;tspan x=&quot;832&quot; y=&quot;199&quot;&gt;biblioteca · lector · launcher · UI&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;1031.2&quot; y=&quot;299&quot; width=&quot;328.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;1195.4&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;1195.4&quot; y=&quot;318&quot;&gt;platox-core&lt;/tspan&gt;&lt;tspan x=&quot;1195.4&quot; y=&quot;336&quot;&gt;Wi-Fi · ajustes · sysinfo · caché&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;658.7&quot; y=&quot;299&quot; width=&quot;346.5&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;832&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;832&quot; y=&quot;318&quot;&gt;platox-ai&lt;/tspan&gt;&lt;tspan x=&quot;832&quot; y=&quot;336&quot;&gt;prompts · streaming · caché offline&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;386.3&quot; y=&quot;299&quot; width=&quot;246.4&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;509.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;509.5&quot; y=&quot;318&quot;&gt;platox-calc&lt;/tspan&gt;&lt;tspan x=&quot;509.5&quot; y=&quot;336&quot;&gt;decimal exacto · teclado&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;32&quot; y=&quot;299&quot; width=&quot;328.3&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;196.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;196.2&quot; y=&quot;318&quot;&gt;platox-sys&lt;/tspan&gt;&lt;tspan x=&quot;196.2&quot; y=&quot;336&quot;&gt;binario platox: hw · splash · rtc&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;g&gt;&lt;path class=&quot;fig-node&quot; d=&quot;M699.7,464 A132.3,6 0 0 1 964.3,464 V540 A132.3,6 0 0 1 699.7,540 Z&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-node-line&quot; d=&quot;M699.7,464 A132.3,6 0 0 0 964.3,464&quot;&gt;&lt;/path&gt;&lt;/g&gt;&lt;text class=&quot;fig-text&quot; x=&quot;832&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;832&quot; y=&quot;488&quot;&gt;Proveedor LLM (tu clave)&lt;/tspan&gt;&lt;tspan x=&quot;832&quot; y=&quot;506&quot;&gt;OpenAI · Claude · DeepSeek&lt;/tspan&gt;&lt;tspan x=&quot;832&quot; y=&quot;524&quot;&gt;o servidor en tu red&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;Tres capas: arranque verificado, Plato como UI, y los crates de Rust detrás&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;las-funcionalidades-nuevas-de-plato&quot;&gt;Las funcionalidades nuevas de Plato&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3 id=&quot;en-platox-público-open-source&quot;&gt;En PlatoX público (open source)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plato como launcher&lt;/strong&gt; (parche 0001): rutas desde el arrancador, entradas por nombre. Es lo que permite que Plato &lt;em&gt;sea&lt;/em&gt; el sistema y no una app más.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Panel de red y selector de Wi-Fi&lt;/strong&gt; (parche 0002): un &lt;code&gt;view/network_panel.rs&lt;/code&gt; nuevo (+281 líneas) que abre, desde cualquier vista, un panel con el estado del Wi-Fi en frases legibles (&lt;code&gt;Apagado / Buscando una red conocida… / Conectado a X / No hay ninguna red conocida cerca&lt;/code&gt;), IP y señal. El icono de la barra tiene cuatro estados (&lt;code&gt;wifi-off&lt;/code&gt;, &lt;code&gt;wifi-connecting&lt;/code&gt;, &lt;code&gt;wifi&lt;/code&gt;, &lt;code&gt;wifi-secure&lt;/code&gt;) y un hilo &lt;em&gt;poller&lt;/em&gt; lo actualiza cada 2 s, nunca desde el hilo de la UI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asistente de IA&lt;/strong&gt; (parches 0004 y 0009): sobre una selección de texto, &lt;strong&gt;Resumir, Traducir o Explicar&lt;/strong&gt;; sobre un libro, &lt;em&gt;insights&lt;/em&gt; —reseña corta, reseña extendida, resumen o un &lt;strong&gt;mapa de conceptos&lt;/strong&gt; navegable (&lt;code&gt;view/concept_map.rs&lt;/code&gt;, +801 líneas)—. Funciona &lt;strong&gt;con tu propia clave&lt;/strong&gt;: OpenAI, &lt;strong&gt;Claude&lt;/strong&gt; (vía la Messages API nativa con &lt;em&gt;streaming&lt;/em&gt; por SSE, con &lt;code&gt;claude-opus-5-5&lt;/code&gt; preseleccionado si tu clave lo lista), DeepSeek o cualquier servidor compatible en tu red. Las respuestas se &lt;strong&gt;cachean offline&lt;/strong&gt; y el Wi-Fi solo se enciende cuando una petición lo necesita, apagándose tras cinco minutos de inactividad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calculadora estilo teléfono&lt;/strong&gt; (parche 0006): reemplaza la vieja calculadora de línea de comandos de Plato por una de teclas. Detrás va el crate &lt;code&gt;platox-calc&lt;/code&gt;, con un &lt;strong&gt;decimal exacto&lt;/strong&gt; (&lt;code&gt;0,1 + 0,2 = 0,3&lt;/code&gt;, no &lt;code&gt;0,30000000000000004&lt;/code&gt;), porcentaje y descuento (&lt;code&gt;120 − 15 % = 102&lt;/code&gt;, con la línea &quot;Descuento: 18&quot;), raíz cuadrada y división por cero controlada.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menú limpio, About y System Info&lt;/strong&gt; (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 &lt;strong&gt;arregla un bug del propio Plato&lt;/strong&gt;, que mostraba la carga del sistema multiplicada por 100 con un signo de &quot;%&quot;, y la memoria libre en vez de la disponible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Herramientas de desarrollo&lt;/strong&gt; (parche 0007): &lt;em&gt;Test All&lt;/em&gt; y &lt;em&gt;Benchmark&lt;/em&gt;, detrás de un marcador de desarrollador, con &lt;strong&gt;redacción de secretos&lt;/strong&gt; en los diagnósticos (nada de api-keys, tokens o PSK en los logs).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;en-kobox-firmware-personal-fuera-de-la-versión-pública&quot;&gt;En KoboX (firmware personal, fuera de la versión pública)&lt;/h3&gt;
&lt;p&gt;Esto existe y funciona, pero depende de infraestructura propia y se quitó del público para reducir superficie: &lt;strong&gt;Tailscale&lt;/strong&gt; (primero en modo usuario, luego con TUN y módulo propio), una &lt;strong&gt;terminal&lt;/strong&gt; (shell de busybox sobre un pty, con &lt;code&gt;fastfetch&lt;/code&gt; y &lt;code&gt;ssh&lt;/code&gt;), &lt;strong&gt;Markets&lt;/strong&gt; (datos de Yahoo), &lt;strong&gt;Weather&lt;/strong&gt; (Open-Meteo), &lt;strong&gt;News&lt;/strong&gt; (un generador propio con &lt;code&gt;trafilatura&lt;/code&gt; en un contenedor endurecido contra SSRF), &lt;strong&gt;Home Assistant&lt;/strong&gt; (luz y clima por un túnel CONNECT) y &lt;strong&gt;sincronización de PDF&lt;/strong&gt; contra un Calibre-Web propio.&lt;/p&gt;
&lt;h2 id=&quot;la-optimización-final-más-rápido-más-robusto-más-responsive-más-actualizado&quot;&gt;La optimización final: más rápido, más robusto, más responsive, más actualizado&lt;/h2&gt;
&lt;p&gt;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: &lt;strong&gt;medir en el aparato, no en el host&lt;/strong&gt;. Estas son las medianas, iteración a iteración, de la versión 1.4:&lt;/p&gt;



































&lt;div class=&quot;table-wrap&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Tabla&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Métrica (mediana)&lt;/th&gt;&lt;th&gt;Antes (beta7)&lt;/th&gt;&lt;th&gt;Después (beta12)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Pasar página (EPUB)&lt;/td&gt;&lt;td&gt;231 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;31 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cambio de capítulo&lt;/td&gt;&lt;td&gt;2967 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;46 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Abrir / reabrir libro&lt;/td&gt;&lt;td&gt;1265 ms&lt;/td&gt;&lt;td&gt;&lt;strong&gt;74 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Primer &lt;em&gt;paint&lt;/em&gt; tras encender&lt;/td&gt;&lt;td&gt;9,4 s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;6,9 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Wi-Fi conectado&lt;/td&gt;&lt;td&gt;12–13 s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;5,0 s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--chart&quot; viewBox=&quot;0 0 864 360&quot; width=&quot;864&quot; height=&quot;360&quot; role=&quot;img&quot; aria-labelledby=&quot;chart-platox-fluidez-t chart-platox-fluidez-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 864px&quot;&gt;&lt;title id=&quot;chart-platox-fluidez-t&quot;&gt;Gráfico de barras del cambio de capítulo en milisegundos de beta7 a beta12: 2967, 2816, 834, 260, 113 y 46 ms, con la caída grande al introducir la caché de layout en disco&lt;/title&gt;&lt;desc id=&quot;chart-platox-fluidez-d&quot;&gt;beta7: 2.967; beta8: 2.816; beta9: 834; beta10: 260; beta11: 113; beta12: 46&lt;/desc&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,316 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;316&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;0&lt;/text&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,220.7 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;220.7&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;1.000&lt;/text&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,125.3 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;125.3&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;2.000&lt;/text&gt;&lt;path class=&quot;fig-grid&quot; d=&quot;M68.5,30 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;62.5&quot; y=&quot;30&quot; text-anchor=&quot;end&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;3.000&lt;/text&gt;&lt;path class=&quot;fig-axis&quot; d=&quot;M68.5,316 H854&quot;&gt;&lt;/path&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;10&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot; transform=&quot;rotate(-90 10 173)&quot;&gt;Cambio de capítulo (ms, mediana)&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;107&quot; y=&quot;33.1&quot; width=&quot;54&quot; height=&quot;282.9&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;134&quot; y=&quot;24.1&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;2967&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;134&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta7&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;237.9&quot; y=&quot;47.5&quot; width=&quot;54&quot; height=&quot;268.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;264.9&quot; y=&quot;38.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;2816&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;264.9&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta8&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;368.8&quot; y=&quot;236.5&quot; width=&quot;54&quot; height=&quot;79.5&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;395.8&quot; y=&quot;227.5&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;834&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;395.8&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta9&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;499.7&quot; y=&quot;291.2&quot; width=&quot;54&quot; height=&quot;24.8&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;526.7&quot; y=&quot;282.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;260&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;526.7&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta10&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--primary&quot; x=&quot;630.6&quot; y=&quot;305.2&quot; width=&quot;54&quot; height=&quot;10.8&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;657.6&quot; y=&quot;296.2&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;113&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;657.6&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta11&lt;/text&gt;&lt;rect class=&quot;fig-bar fig-bar--highlight&quot; x=&quot;761.5&quot; y=&quot;311.6&quot; width=&quot;54&quot; height=&quot;4.4&quot; rx=&quot;1&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;788.5&quot; y=&quot;302.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;46&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;788.5&quot; y=&quot;328&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;beta12&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;461.3&quot; y=&quot;352&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;Iteración (medido en el Kobo)&lt;/text&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;461.3&quot; y=&quot;53.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot; font-size=&quot;11&quot;&gt;caché de layout en disco + worker de siguiente capítulo: 2967 → 46 ms&lt;/text&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;El cambio de capítulo, de 2967 a 46 ms: dónde cayó y por qué&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id=&quot;más-rápido&quot;&gt;Más rápido&lt;/h3&gt;
&lt;p&gt;El trabajo (parche 0003) ataca cada capa del coste:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pasar página:&lt;/strong&gt; una copia directa de gray8 a RGB565 con una tabla de 256 entradas (&lt;code&gt;framebuffer/fast16.rs&lt;/code&gt;) y el &lt;em&gt;prerender&lt;/em&gt; de la página vecina en un hilo aparte (nice 5). El &lt;em&gt;page turn&lt;/em&gt; de beta8 cayó de 230,7 a 42,8 ms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Texto y layout:&lt;/strong&gt; caché de &lt;em&gt;shaping&lt;/em&gt; por palabra, buffers de HarfBuzz reutilizados, patrones de guionado cargados en diferido por idioma, CSS parseado una sola vez por documento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capítulos sin esperas:&lt;/strong&gt; layout del siguiente capítulo en un &lt;em&gt;worker&lt;/em&gt; y &lt;strong&gt;caché de layout en disco&lt;/strong&gt; (&lt;code&gt;prefetch.rs&lt;/code&gt;, &lt;code&gt;layoutcache.rs&lt;/code&gt;, &lt;code&gt;dlcodec.rs&lt;/code&gt;), con verificación por dev/inode/tamaño/mtime. Esa es la barra que se desploma en el gráfico.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compilación:&lt;/strong&gt; MuPDF, HarfBuzz, FreeType y DjVuLibre compilados desde fuente con &lt;strong&gt;LTO y NEON&lt;/strong&gt; (el layout bajó de 13,4 a &lt;strong&gt;9,8 ms/KB&lt;/strong&gt;), y el perfil de release con &lt;code&gt;lto&lt;/code&gt; &lt;em&gt;fat&lt;/em&gt; y &lt;code&gt;codegen-units = 1&lt;/code&gt; y &lt;code&gt;target-cpu=cortex-a9&lt;/code&gt;. El binario encogió de 10.494.280 a 8.851.200 bytes (−16 %).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E-ink:&lt;/strong&gt; refresco parcial entre páginas y refresco completo cada 20 para controlar el &lt;em&gt;ghosting&lt;/em&gt;, más &lt;code&gt;zram&lt;/code&gt; para estirar los 512 MB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;más-responsive&quot;&gt;Más responsive&lt;/h3&gt;
&lt;p&gt;La responsividad no es solo la UI. El Wi-Fi tardaba 10 segundos en escanear por un &lt;em&gt;timeout&lt;/em&gt; fijo del driver &lt;code&gt;wext&lt;/code&gt;; cambiándolo a &lt;code&gt;nl80211,wext&lt;/code&gt;, el escaneo bajó de 10,5 a &lt;strong&gt;1,4 s&lt;/strong&gt; y la autoconexión a &lt;strong&gt;5,0 s&lt;/strong&gt;. El apagado y reinicio se midió en &lt;strong&gt;0,18–0,23 s&lt;/strong&gt; entre la petición y el desmontaje limpio del onboard.&lt;/p&gt;
&lt;h3 id=&quot;más-robusto&quot;&gt;Más robusto&lt;/h3&gt;
&lt;p&gt;La robustez es, en este proyecto, una propiedad de seguridad. El corazón es &lt;code&gt;platox-boot&lt;/code&gt;, un programa en &lt;strong&gt;C estático&lt;/strong&gt; instalado como PID 1 en la partición de sistema de Kobo. En cada arranque verifica el kernel, un &lt;code&gt;boot.manifest&lt;/code&gt; y el &lt;strong&gt;sha256&lt;/strong&gt; de la imagen del sistema; y si algo falla —o si se encadenan &lt;strong&gt;tres arranques que no llegan a Plato&lt;/strong&gt;— cede el control al init de serie, Nickel. El primer &lt;em&gt;paint&lt;/em&gt; de Plato resetea ese contador.&lt;/p&gt;
&lt;figure class=&quot;figure figure--svg&quot;&gt;&lt;div class=&quot;figure-plate figure-plate--svg&quot;&gt;&lt;svg class=&quot;fig fig--flow&quot; viewBox=&quot;2 2 626.9 810.8&quot; width=&quot;627&quot; height=&quot;811&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-platox-boot-chain-es-t fig-platox-boot-chain-es-d&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; style=&quot;--fig-w: 627px&quot;&gt;&lt;title id=&quot;fig-platox-boot-chain-es-t&quot;&gt;Diagrama de decisión de platox-boot: comprueba kernel conocido, manifiesto y sha256 válidos, y menos de tres intentos fallidos; si todo pasa desempaqueta el sistema en RAM y arranca PlatoX, y cualquier fallo cae a Nickel&lt;/title&gt;&lt;desc id=&quot;fig-platox-boot-chain-es-d&quot;&gt;/sbin/init = platox-boot C estático, en la partición de Kobo → ¿Kernel conocido y .platox/enabled presente?; ¿Kernel conocido y .platox/enabled presente? → boot.manifest: ¿tamaño y sha256 cuadran? (sí); boot.manifest: ¿tamaño y sha256 cuadran? → ¿menos de 3 arranques que no llegaron a Plato? (sí); ¿menos de 3 arranques que no llegaron a Plato? → Sistema a RAM tmpfs + pivot_root (sí); Sistema a RAM tmpfs + pivot_root → PlatoX Plato 0.9.45 + parches; ¿Kernel conocido y .platox/enabled presente? → init de serie: Nickel la red de seguridad (no); boot.manifest: ¿tamaño y sha256 cuadran? → init de serie: Nickel la red de seguridad (no); ¿menos de 3 arranques que no llegaron a Plato? → init de serie: Nickel la red de seguridad (no: 3 fallos)&lt;/desc&gt;&lt;defs&gt;&lt;marker id=&quot;fig-platox-boot-chain-es-arrow&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;marker id=&quot;fig-platox-boot-chain-es-arrow-accent&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; class=&quot;fig-arrow fig-accent-fill&quot;&gt;&lt;/path&gt;&lt;/marker&gt;&lt;/defs&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M407.6,68 L407.6,87 Q407.6,95 407.6,103 L407.6,122&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M349.3,217.6 L309.2,250.5 Q303,255.6 303,263.6 L303,293.6&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;288.9&quot; y=&quot;244.6&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;303&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;303&quot; y=&quot;255.6&quot;&gt;sí&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M242.6,389.2 L200.9,422.2 Q194.6,427.2 194.6,435.2 L194.6,465.2&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;180.5&quot; y=&quot;416.2&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;194.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;194.6&quot; y=&quot;427.2&quot;&gt;sí&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M170.2,560.8 L154.4,591.7 Q150.8,598.8 150.8,606.8 L150.8,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;136.7&quot; y=&quot;587.8&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;150.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.8&quot; y=&quot;598.8&quot;&gt;sí&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--solid&quot; d=&quot;M150.8,692.8 L150.8,711.8 Q150.8,719.8 150.8,727.8 L150.8,746.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M479.9,217.6 L530.7,251.2 Q537.4,255.6 537.4,263.6 L537.4,333.4 Q537.4,341.4 537.4,349.4 L537.4,419.2 Q537.4,427.2 537.4,435.2 L537.4,505 Q537.4,513 537.4,521 L537.4,590.8 Q537.4,598.8 534.3,606.2 L521.2,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;523.3&quot; y=&quot;416.2&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;537.4&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;537.4&quot; y=&quot;427.2&quot;&gt;no&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M367.5,389.2 L412.3,422.4 Q418.7,427.2 418.7,435.2 L418.7,505 Q418.7,513 418.7,521 L418.7,590.8 Q418.7,598.8 425.2,603.5 L470.9,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;404.6&quot; y=&quot;502&quot; width=&quot;28.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;418.7&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;418.7&quot; y=&quot;513&quot;&gt;no&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;path class=&quot;fig-edge fig-edge--dashed&quot; d=&quot;M238.6,560.8 L268.1,592.9 Q273.6,598.8 281.3,601 L409.3,636.8&quot; marker-end=&quot;url(#fig-platox-boot-chain-es-arrow)&quot;&gt;&lt;/path&gt;&lt;g class=&quot;fig-edge-label&quot;&gt;&lt;rect x=&quot;214&quot; y=&quot;587.8&quot; width=&quot;119.2&quot; height=&quot;22&quot; rx=&quot;3&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text fig-text--muted&quot; x=&quot;273.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;273.6&quot; y=&quot;598.8&quot;&gt;no: 3 fallos&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;234.4&quot; y=&quot;12&quot; width=&quot;346.5&quot; height=&quot;56&quot; rx=&quot;28&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;407.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;31&quot;&gt;/sbin/init = platox-boot&lt;/tspan&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;49&quot;&gt;C estático, en la partición de Kobo&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;407.6,122 596.9,169.8 407.6,217.6 218.4,169.8&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;407.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;160.8&quot;&gt;¿Kernel conocido y&lt;/tspan&gt;&lt;tspan x=&quot;407.6&quot; y=&quot;178.8&quot;&gt;.platox/enabled presente?&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;303,293.6 492.2,341.4 303,389.2 113.8,341.4&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;303&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;303&quot; y=&quot;332.4&quot;&gt;boot.manifest:&lt;/tspan&gt;&lt;tspan x=&quot;303&quot; y=&quot;350.4&quot;&gt;¿tamaño y sha256 cuadran?&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;polygon class=&quot;fig-node&quot; points=&quot;194.6,465.2 377.3,513 194.6,560.8 12,513&quot;&gt;&lt;/polygon&gt;&lt;text class=&quot;fig-text&quot; x=&quot;194.6&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;194.6&quot; y=&quot;504&quot;&gt;¿menos de 3 arranques&lt;/tspan&gt;&lt;tspan x=&quot;194.6&quot; y=&quot;522&quot;&gt;que no llegaron a Plato?&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;54.9&quot; y=&quot;636.8&quot; width=&quot;191.8&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;150.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.8&quot; y=&quot;655.8&quot;&gt;Sistema a RAM&lt;/tspan&gt;&lt;tspan x=&quot;150.8&quot; y=&quot;673.8&quot;&gt;tmpfs + pivot_root&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node fig-accent&quot; x=&quot;36.7&quot; y=&quot;746.8&quot; width=&quot;228.2&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;150.8&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;150.8&quot; y=&quot;765.8&quot;&gt;PlatoX&lt;/tspan&gt;&lt;tspan x=&quot;150.8&quot; y=&quot;783.8&quot;&gt;Plato 0.9.45 + parches&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;g class=&quot;fig-node-group&quot;&gt;&lt;rect class=&quot;fig-node&quot; x=&quot;399.8&quot; y=&quot;636.8&quot; width=&quot;219.1&quot; height=&quot;56&quot; rx=&quot;4&quot;&gt;&lt;/rect&gt;&lt;text class=&quot;fig-text&quot; x=&quot;509.3&quot; text-anchor=&quot;middle&quot; dominant-baseline=&quot;central&quot;&gt;&lt;tspan x=&quot;509.3&quot; y=&quot;655.8&quot;&gt;init de serie: Nickel&lt;/tspan&gt;&lt;tspan x=&quot;509.3&quot; y=&quot;673.8&quot;&gt;la red de seguridad&lt;/tspan&gt;&lt;/text&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/div&gt;&lt;figcaption&gt;El selector de arranque: tres comprobaciones y una red de seguridad&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;El escritor de la ranura del kernel es igual de cuidadoso: escribe con &lt;code&gt;dd conv=notrunc,fsync&lt;/code&gt;, vuelve a leer y comprueba el sha256, y si no cuadra &lt;strong&gt;restaura la copia verificada previa&lt;/strong&gt; sin reiniciar, todo bajo &lt;code&gt;timeout 300&lt;/code&gt; y solo con la batería al 30 % o cargando. El init propio de KoboX 1.4, además, &lt;strong&gt;rechaza paquetes de actualización ajenos&lt;/strong&gt;: solo aplica los suyos, con manifiesto (sha256, tamaño, rutas, espacio, batería), y aparta cualquier &lt;code&gt;KoboRoot.tgz&lt;/code&gt; extraño sin ejecutarlo. La versión estable (Master Edition) remató esto con escrituras atómicas de configuración y &lt;code&gt;clippy -D warnings&lt;/code&gt; limpio en los ocho crates.&lt;/p&gt;
&lt;h3 id=&quot;más-actualizado&quot;&gt;Más actualizado&lt;/h3&gt;
&lt;p&gt;Donde el sistema de serie era un museo de 2015, el userland propio es de hoy: &lt;strong&gt;Buildroot 2025.02 LTS&lt;/strong&gt;, glibc 2.41, busybox 1.37, &lt;code&gt;wpa_supplicant 2.12&lt;/code&gt; con &lt;strong&gt;mbedTLS&lt;/strong&gt; y soporte de &lt;strong&gt;WPA3/SAE/OWE&lt;/strong&gt; — y &lt;strong&gt;sin OpenSSL, sin udev y sin systemd&lt;/strong&gt;. Hay incluso un kernel propio &lt;strong&gt;4.1.15&lt;/strong&gt; recompilado desde las fuentes GPL de Kobo con suspensión fiable: el aparato se colgaba al suspender, y por &lt;strong&gt;bisección de configuración&lt;/strong&gt; encontramos que la causa era, paradójicamente, &lt;em&gt;quitar&lt;/em&gt; &lt;code&gt;CONFIG_PM_DEBUG&lt;/code&gt;; esos mensajes de depuración ocultaban una &lt;em&gt;race&lt;/em&gt; en la suspensión de la placa, así que la solución fue dejarlo como en stock.&lt;/p&gt;
&lt;h2 id=&quot;nota-de-método-y-modelo-de-amenazas&quot;&gt;Nota de método y modelo de amenazas&lt;/h2&gt;
&lt;p&gt;Lo que esto es y lo que no es, dicho sin adornos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hay integridad, no autenticidad.&lt;/strong&gt; &lt;code&gt;platox-boot&lt;/code&gt; verifica el sha256 del sistema en cada arranque, pero &lt;strong&gt;no hay firmas&lt;/strong&gt;. Un atacante local que pueda escribir en la partición onboard puede sustituir a la vez el &lt;code&gt;cpio&lt;/code&gt; y su manifiesto. El modelo de amenazas de este diseño es &lt;strong&gt;anti-brick y anti-corrupción&lt;/strong&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La clave de API va en claro&lt;/strong&gt;, en &lt;code&gt;.platox/config/ai.toml&lt;/code&gt;, 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No llama a casa.&lt;/strong&gt; 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 &lt;code&gt;pool.ntp.org&lt;/code&gt; para la hora. El Wi-Fi está apagado salvo que una acción lo necesite.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El debloat no borra, oculta.&lt;/strong&gt; 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ó.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cadena de suministro:&lt;/strong&gt; fuentes fijadas por versión y sha256, kernel y paquetes reproducibles, y un &lt;code&gt;check-public.sh&lt;/code&gt; que rechaza datos personales, infraestructura privada y ficheros propietarios de Kobo antes de cada publicación.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;lo-que-nos-llevamos&quot;&gt;Lo que nos llevamos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;El actualizador es el vector.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El estado en disco es estado de seguridad.&lt;/strong&gt; Cinco bytes en el MBR decidían si el aparato se borraba. Quien sustituye al sistema, hereda sus invariantes; descubrirlas tarde cuesta un dispositivo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integridad no es autenticidad.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los bricks son datos.&lt;/strong&gt; Cada aparato que dejamos inservible —permisos, espacio, un &lt;code&gt;pickel-imx&lt;/code&gt; de más en una lista de borrado— enseñó una invariante del sistema que ninguna documentación contaba.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id=&quot;recursos&quot;&gt;Recursos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plato&lt;/strong&gt;, de Bastien Dejean — el lector open-source en el corazón de todo: &lt;a href=&quot;https://github.com/baskerville/plato&quot;&gt;github.com/baskerville/plato&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlatoX&lt;/strong&gt; — la edición pública (AGPL-3.0) de este trabajo; sin repositorio público por ahora.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buildroot&lt;/strong&gt; — el sistema embebido que arranca en RAM: &lt;a href=&quot;https://buildroot.org&quot;&gt;buildroot.org&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>sec</category></item></channel></rss>