{"slug": "rooting-an-echo-plus-2-with-ai", "title": "Rooting an Echo Plus 2 with AI", "summary": "A developer rooted an Amazon Echo Plus 2 and achieved persistent root that survives a normal cold boot, using OpenAI's Codex coding agent running the GPT-6 Sol model to work out the exploit chain, with code and a step-by-step guide published in the project repository. The effort built on prior work on related Echo devices — EchoCLI, firebreak, the LineageOS Echo kiosk work, and echo-dot-assist — and targeted the Echo Plus 2's MediaTek chip and Android-derived boot chain, from BootROM through preloader, Little Kernel, boot image, and the Linux kernel. The goal was to run a self-chosen model wired into Home Assistant on existing Echo speakers and microphones rather than Amazon's Alexa+.", "body_md": "# Rooting an Echo Plus 2 with AI\n\nToday's AI models hold a conversation, use tools, and handle the kind of open-ended requests that older voice assistants reduced to a handful of fixed commands. Amazon sells its own version of this now, [Alexa+](https://www.aboutamazon.com/what-we-do/devices-services/alexa-plus). But I wanted to choose the model myself, wire it into my Home Assistant setup, and decide where my data goes, all while still using the speakers and microphones I already owned.\n\nAs more of my home automation moved to Home Assistant, Alexa started to feel like the odd one out. I tried a [Home Assistant Voice Preview Edition](https://www.home-assistant.io/voice-pe/): I loved that it was open, but I missed how my Echos sounded. Then I rooted an Echo Show 5, put [Kiosk Satellite](https://github.com/jxlarrea/kiosk-satellite) on it, and finally had a voice assistant I actually enjoyed using. Which raised the obvious question: could I do the same to another Echo?\n\nI picked the Echo Plus 2. It was the oldest Echo I owned, which made it the best bet for known vulnerabilities, and it happened to sound great. I wasn't starting from scratch, either: [EchoCLI](https://github.com/Dragon863/EchoCLI), [firebreak](https://github.com/bboe/firebreak), the [LineageOS work](https://github.com/jxlarrea/lineageos-echo-kiosk-satellite/blob/main/docs/unlocking.md), and [echo-dot-assist](https://github.com/Gamer92000/echo-dot-assist) had already mapped a lot of the terrain on related devices. None of it dropped straight onto my Plus 2, though. Two questions were still open. Could I run my own code on this thing, and how much of the work could I hand to an AI?\n\nThis post follows the answer to both, worked out together with Codex, OpenAI's coding agent, running their GPT-6 Sol model. The short version: we ended up with root that comes back on its own after a normal cold boot. If you want to reproduce it, the code and a step-by-step guide are in the [project repository](https://github.com/Tripton/echo-plus-2-root).\n\n## Where could my code run?\n\nWork on related Echos suggested this one ran an Android-derived system too, and the hardware pointed at a MediaTek chip, which made a pile of existing MediaTek exploits worth a look. On an Android device, control comes in layers. An ordinary app lives inside a sandbox. The Linux kernel enforces that sandbox and owns the hardware. And code in an early boot stage can decide what loads before Android even exists. The earlier you sit in that chain, the more power you have, but those early stages are also small, with far fewer ways in.\n\nFigure 1 is the startup path I used as a map. The arrows show what normally runs next; the labels say what each stage is for, before any exploit enters the picture.\n\n```\n +--------------------------+\n | BootROM                  |  Fixed instructions in the chip\n +--------------------------+\n              |\n              v\n +--------------------------+\n | Preloader                |  Prepare memory and board hardware\n +--------------------------+\n              |\n              v\n +--------------------------+\n | Little Kernel (LK)       |  Select and check the boot image\n +--------------------------+\n              |\n              v\n +--------------------------+\n | Boot image               |  Supply the kernel and initial file system\n +--------------------------+\n              |\n              v\n +--------------------------+\n | Linux kernel and Android |  Enforce app sandboxes and start services\n +--------------------------+\n              |\n              v\n +--------------------------+\n | Installed apps           |  Run with ordinary app privileges\n +--------------------------+\n```\n\n*Figure 1. The normal startup path and the job of each stage.*\n\nEach of these stages earns its place. The BootROM gives the chip a fixed starting point and a door into a low-level recovery mode. MediaTek can't know every board's memory, clocks, and peripherals at manufacture time, so the preloader brings up this particular board. LK can pick between boot slots, which lets an update stage a new image while keeping a fallback. And the boot image carries the Linux kernel and the initial file system Android needs to come up.\n\nThe checks between these stages form a **Secure Boot chain**: each verifier decides whether the next image is allowed to run. [Android's Verified Boot documentation](https://source.android.com/docs/security/features/verifiedboot) explains the model, and I later confirmed that this Plus 2's LK does reject an altered boot image. But my first goal was smaller than breaking that chain: run my own code, learn how the device starts, and get enough control to use its microphone, buttons, and network on my terms. Kernel-level control would cover most of that.\n\n## Getting initial code execution\n\nPrevious MediaTek BootROM research had already produced the tools I'd need. Codex helped me pin this chip down as a promising target. The one thing standing between me and those tools was a USB data connection to the chip. Teardowns of other Echos versions made an exposed interface plausible.\n\nSix small contacts sit on the underside of the assembled Echo Plus 2. **You don't have to open the enclosure to reach them.** I opened mine anyway, to inspect the traces and take measurements, then soldered a connection for stable debugging. I checked ground and the suspected USB lines with a multimeter and talked the readings over with Codex. My cable used TM4 for USB D+ (green lead), TM5 for USB D− (white lead), and TM37 for ground (blue lead). Spring-loaded pogo pins can reach the same exterior contacts. Figure 2 shows the mapping I used.\n\n*Figure 2. The exterior contacts and their corresponding pads on the opened board.*\n\nHolding the **Action** button while applying power brought the Plus 2 up over USB in BootROM mode. [MTKClient](https://github.com/bkerler/mtkclient) reported hardware code `0x8167`, and its **Kamakiri** method worked on my unit out of the box. The first thing I ran was a read-only identification command, with power applied while holding Action:\n\n```\n\"$HOME/mtkclient/.venv/bin/python\" \"$HOME/mtkclient/mtk.py\" gettargetconfig --skipwdt\n```\n\n[R0rt1z2's write-up](https://blog.r0rt1z2.com/posts/dissecting-a-mantis/) explains Kamakiri in detail. The exploit researchers and the MTKClient maintainers did the foundational work here. Our [reproduction guide](https://github.com/Tripton/echo-plus-2-root#prepare-the-computer) pins the exact tool version and lists the prep commands.\n\nKamakiri handed me control at the very first box in Figure 1. Powerful but temporary, and just as usefully, it let me *look* instead of guess. A separate read-only session captured the storage layout and the boot images. The two partition tables both checked out and showed an active A slot, a spare B slot, and a shared `/data` partition. Figure 3 maps it.\n\n```\n slot A   boot_a    system_a   <- active on the measured boot\n slot B   boot_b    system_b   <- available for an update or fallback\n shared   /data                <- installed apps and their data\n```\n\n*Figure 3. A simplified map of storage measured on my Plus 2.*\n\nWhat I actually wanted was to run an Android voice app on the original audio hardware. That meant letting Android finish booting while holding on to enough control to install apps, and a bare BootROM entry point doesn't give you that.\n\nSo I asked Codex to build the bridge. It sounds like one step. It isn't: the payload has to keep the stock boot sequence intact across several stages, then change what happens *after* LK has already checked the boot image. Codex staged a copy of the stock preloader in RAM, rewrote LK's final handoff, and slipped in a temporary initial-file-system overlay. That overlay brought up a root-capable USB ADB service while stock Android kept booting around it. We watched the whole thing come up in layers: boot milestones, ADB identity and capabilities, Android's Package Manager, and finally Alexa answering. At that point I could install an app the normal way, through Android's package installer. Its code landed in `/data`, where Android expects apps to live, and the signed boot images were never touched.\n\nMost of that bridge was Codex's engineering, not mine. My contribution was almost embarrassingly physical: I taped the Action button down and plugged the Echo into a smart plug I could toggle from anywhere. That was enough to close the loop. Codex could now flash a payload, cycle the power, inspect what booted, rewrite the payload, and go again, over and over, without me touching the bench. It ground through failure after failure overnight and had Android *and* a root ADB shell up by the time I woke. Pulling a device-specific preloader, a patched LK handoff, a temporary file system, and a working Android session into one payload is serious low-level work, and I couldn't have built and tested it myself in anything like the time. A $20 smart plug is what turned an experiment that needed me at the bench into one the agent could run while I slept.\n\nWith the bridge live, I could finally ask the running system what it was. These read-only ADB commands gave me the details:\n\n```\nadb -d shell getprop ro.build.fingerprint\nadb -d shell getprop ro.build.version.security_patch\nadb -d shell uname -r\nadb -d shell uname -v\n```\n\nThe Plus 2 reported Fire OS 6.5.7.5 (`NS6575/8338`), fingerprint `Amazon/lidar/lidar:6.0/NS6575/8338N:user/amz-p,release-keys`, a security patch property of `2026-03-01`, and kernel `4.4.22+` built on **14 October 2026**. I only learned any of this *after* the bridge worked. None of it was an assumption going in.\n\nThe same read-only session also dumped storage and the boot images, which handed us this unit's actual kernel for offline analysis. From here on, Codex could test a candidate bug against the exact code the Plus 2 would run before ever putting another payload on the device.\n\n## Making the access survive a reboot\n\nWith the RAM bridge running, I installed Kiosk Satellite. Its microphone and speaker worked straight away over Home Assistant's voice path, on the stock Android audio stack. Kiosk itself is just an ordinary app; root is what let us hang on to microphone access after Fire OS tried to revoke it, drive the physical buttons and LED ring, and poke at stock services. Then I cut power, and exactly as expected, the BootROM payload and every temporary boot change vanished. Re-running a physical exploit after every restart is no way to live with a speaker you use every day.\n\nThe textbook answer would be to find a bug in an earlier boot stage and smuggle a modified image past the signature checks. Codex traced LK and ran bounded RAM probes, but the promising bugs never led to a path you could actually reach on an ordinary cold boot. So I redirected the search. Android had already accepted the app I installed through the bridge, and unlike a modified boot image, an installed app in `/data` simply survives a reboot.\n\nI ran the bridge once more and installed a small research APK with a helper binary at `/data/x`. The APK registered a `LOCKED_BOOT_COMPLETED` receiver, so stock Android started it on every normal cold boot, no USB exploit required. Secure Boot still verified the boot images. The Package Manager just loaded our signed app from `/data` into its usual sandbox. The bridge was only ever needed to get the app *installed* on a locked device. After that, I had **code execution on every cold boot**, at first with nothing but ordinary app privileges.\n\nWhich left the real question: could that sandboxed app reach the kernel? Linux and Android kernel bugs are the classic route out of a sandbox, so I asked Codex to hunt for one reachable from an unprivileged app on this old kernel, and to check every candidate against the image we'd captured. One stood out: the Binder use-after-free [Google Project Zero analyzed in 2019](https://projectzero.google/2019/11/bad-binder-android-in-wild-exploit.html), CVE-2019-2215. An October 2026 build still carrying a 2019 bug sounded far-fetched. We treated the static match as a lead, nothing more, and went to test it on the live device.\n\n## The Binder lifetime bug\n\nBinder is how one Android process asks another to do work. Picture an app asking a system service to change the volume. The calling thread can wait for the reply, and to make that possible the kernel gives it a **wait queue**. Separately, Linux `epoll` lets an app watch several event sources at once; when it watches Binder here, it holds a pointer straight to that wait queue (Figure 4).\n\n``` php\n app process                         kernel\n\n request to service  ----------->  Binder thread record\n                                        |\n                                        +-- wait queue\n                                               ^\n                                               |\n                          epoll watch ----------+\n```\n\n*Figure 4. The Binder thread's wait queue and the epoll watch.*\n\nA pointer is only safe while the thing it points at still exists. When the Binder thread exits, the driver frees its record, and the wait queue inside it. The *right* order is to tell epoll to let go of its pointer first; the [Android fix](https://android.googlesource.com/kernel/common/+/82bd53b8713775eeacd21c12fc5ca1964337e8b6%5E!/) does exactly that, with a `POLLFREE` notification. Skip it, and epoll is left holding a pointer into memory that now belongs to something else entirely. That's the **use-after-free**. Figure 5 puts the right order and this build's broken one side by side.\n\n``` php\n intended    notify epoll -> remove its pointer -> free Binder thread\n\n Plus 2      free Binder thread -> epoll still points into old memory\n                                          |\n                         another allocation can occupy those bytes\n                                          |\n                         epoll cleanup treats them as a Binder queue\n```\n\n*Figure 5. The lifetime rule and the missing cleanup on this build.*\n\nThe missing piece is tiny. Here's the [Android fix](https://android.googlesource.com/kernel/common/+/82bd53b8713775eeacd21c12fc5ca1964337e8b6%5E!/), boiled down to the lines that matter:\n\n``` php\nif ((thread->looper & BINDER_LOOPER_STATE_POLL) &&\n    waitqueue_active(&thread->wait))\n    wake_up_poll(&thread->wait, POLLHUP | POLLFREE);\n/* The thread and its wait queue can now be freed. */\n```\n\nCodex followed `BINDER_THREAD_EXIT` through the saved Plus 2 kernel and found no equivalent notification before the free. Then the app put the lifetime rule to the test on real hardware. The trigger leaves a Binder request pending for a service in another process; on this Binder version, that pending request is what makes epoll latch onto the short-lived **thread** queue instead of a longer-lived process queue. That detail is the whole ballgame. (`EPOLLIN` just means \"wake me when this source has something to read.\")\n\n```\n/* A Binder request is waiting for another process to reply. */\nstruct epoll_event event = {.events = EPOLLIN};\nepoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event); // epoll saves queue pointer\nioctl(binder_fd, BINDER_THREAD_EXIT, 0);            // Binder frees the queue\n/* Other kernel data can now occupy the freed block. */\nepoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, NULL);    // stale pointer is used\n```\n\nThe `ioctl` frees the queue; epoll keeps its address anyway. So that last call reaches into whatever moved into the freed memory next. The [full source](https://github.com/Tripton/echo-plus-2-root/blob/main/src/persistence/app/compose.c) handles the return-value checks and the pending request I've elided here. Codex confirmed the reuse on the actual Plus 2, then set about turning it into a controlled write. Google's Pixel exploit was a map to the root cause, not a program we could run, as its memory layout didn't fit this device.\n\n### What can occupy the freed memory?\n\nFreeing an object doesn't wipe its bytes. Linux is free to hand that same block to the next allocation of the right size, and epoll is still pointing into it. When the stale watch is torn down, its cleanup rewrites two pointers of a doubly linked list, and on this layout those writes land at offsets `0x50` and `0x58` inside whatever now occupies the block. So the question became very concrete: **what do we want sitting at those two offsets?**\n\nCodex started with something harmless and readable: 96-byte messages queued on a local Unix socket. The app could fill one with recognizable bytes, let it take over the freed block, then read it back. Removing the stale epoll watch changed two words near its end. This was never part of the exploit chain, as it was a **ruler**, a way to *see* exactly where epoll writes before trying to make that write do anything useful. Here's one captured message (Figure 6):\n\n```\n                              +0x00                +0x50          +0x58\n queued 96-byte message       [ chosen bytes ... ][ 0000000000000000 ][ 0000000000000000 ]\n after epoll cleanup          [ chosen bytes ... ][ ffffffc01ad71050 ][ ffffffc01ad71050 ]\n\n list-node address q = 0xffffffc01ad71050\n buffer address    P = q - 0x50 = 0xffffffc01ad71000\n```\n\n*Figure 6. Two words in a retained message changed after stale epoll cleanup.*\n\nThe changed words leaked a kernel address and, with the known offset, the base of that one message. Useful for confirmation, but a leak isn't a write. The real chain swaps the message for an object whose fields at `0x50` and `0x58` can bend the kernel into writing somewhere *we* choose.\n\n### Then, redirect a receive operation\n\nThat object is the kernel's private copy of an **iovec** array: the list `recvmsg` uses when it receives data from a socket. Each entry says two things: where to put bytes, and how much room is there.\n\n```\nstruct iovec {\n    void  *iov_base;  // where to write received bytes\n    size_t iov_len;   // how many bytes fit there\n};\n```\n\nHere's the one property everything hinges on: as `recvmsg` fills entry after entry, it re-reads `iov_base` and `iov_len` **fresh from memory every time it continues**. It just remembers \"I'm in entry *N*, at position *X*.\" If that memory changes mid-receive, the kernel keeps going with the new values, and that memory is now ours.\n\nWe split the job across two processes of our own app. The **child** calls `recvmsg` with a list of perfectly ordinary user-space destinations. The kernel checks them with `access_ok`, copies the list into kernel memory, and settles in to wait for data, and *that* copied list is what reuses the freed Binder block. The **parent** feeds bytes to the waiting child and pulls the stale epoll watch at the right instant. Handing `recvmsg` a kernel address up front would never survive its check. Editing the already-checked copy, after the check, is the opening. A textbook **time-of-check / time-of-use** flaw.\n\nThe geometry lines up almost too well. Each iovec entry is 16 bytes, so entry 5 starts at `5 × 16 = 0x50`: its base at `0x50`, its length at `0x58`, and entry 6's base right after at `0x60`. Those are exactly the fields epoll's cleanup can reach. Figure 7 walks the whole thing byte by byte, with the array placed at an illustrative `0x1000` so the addresses are easy to follow.\n\n```\n Say the copied iovec array sits at 0x1000. recvmsg has already\n passed its one and only address check -- nobody checks again.\n\n   0x1000  entry[5].base = USER_A   (your buffer, allowed)\n   0x1008  entry[5].len  = 24\n   0x1010  entry[6].base = USER_B   (your buffer, allowed)\n\n (1) send 8 bytes       -> USER_A        recvmsg is now 8 bytes into entry[5]\n\n (2) trigger epoll bug  -> entry[5].base := 0x1000\n                           it now points at ITSELF, so from here recvmsg\n                           writes straight into the iovec array\n\n (3) send 16 bytes      -> lands at 0x1000 + 8 = 0x1008, then 0x1010:\n                             0x1008  entry[5].len  := 24\n                             0x1010  entry[6].base := &selinux_enforcing\n                           entry[5] is now \"full\" (8 + 16 = 24) -> advance to entry[6]\n\n (4) send 4 bytes       -> &selinux_enforcing      00 00 00 00  -> SELinux off\n```\n\n*Figure 7. Two pointer writes we don't control become one write we do.*\n\nWalk it slowly, because the trick is subtle. The epoll bug gives us a single kernel write we can't choose the value of, but it points `entry[5].base` back at itself, and *that* flips the receive from \"write out to my buffer\" to \"write into my own control table.\" Step 3's 16 bytes are plain socket data, fully ours; they just happen to land on the iovec metadata, resetting entry 5's length to 24 (so the kernel cleanly moves on) and dropping a real kernel address into `entry[6].base`, the value `access_ok` would have rejected on sight. Step 4's final bytes then flow through entry 6 to that address. Two pointers we couldn't choose, combined into one we can.\n\nAnd that last step is the whole payoff: once `entry[6].base` holds an address we picked, the bytes we send next get written *there*, and they're nothing but socket data we type. So what we've really built is a controlled write: chosen bytes at a chosen kernel address. The only cap per shot is `entry[6]`'s own length field, which decides how many bytes land before the receive moves on; whether that's four bytes or eight is simply the size of the thing we're overwriting, not anything the bug imposes. As a guard, the parent checks that the 16 redirection bytes did **not** show up in the original user buffer; if they did, the pivot missed and it sends nothing further.\n\nOne thing worth stating plainly, because it's easy to wonder about: these are *fixed* kernel addresses, not values we recompute each boot. This build doesn't randomize its kernel layout (there's no KASLR in the way), so an address read out of the static image is exactly where it sits on the running device, every time. The 50/50 reboot series below is the practical proof: the same two addresses held across every single boot.\n\nCodex pulled those two targets from this unit's captured kernel: `selinux_enforcing` at `0xffffff8008c301c4`, `core_pattern` at `0xffffff8008b59398`. Each needs its own free-and-reuse attempt, and the [source](https://github.com/Tripton/echo-plus-2-root/blob/main/src/persistence/app/compose.c) carries the timing, the guards, and the checks against each location's original value. A controlled write to two chosen globals is not a general kernel memory editor, but it doesn't need to be, because those two globals are exactly the ones that hand you code execution.\n\nIf you know [Project Zero's 2019 write-up](https://projectzero.google/2019/11/bad-binder-android-in-wild-exploit.html), this will look familiar and different at the same time. Their PoC leans on iovecs twice: `writev` to *leak* a kernel address, then `recvmsg` to overwrite `addr_limit` and open up arbitrary read/write across the whole kernel. Ours uses the same `recvmsg` trick for the write step, but reaches the address a different way and skips `addr_limit` entirely: it drops values straight into those two specific globals instead. Worth keeping in mind that Google itself notes its PoC isn't necessarily how the in-the-wild exploit worked; it's one route to the bug, and this is another.\n\nNow the two writes themselves. The first, into `selinux_enforcing`, flips it from `1` to `0`. SELinux normally enforces extra rules on what a process may touch or run; zero makes it **Permissive**, so violations get logged but not blocked. The chain needs that, because the next step runs a helper from `/data`, outside the trusted system paths.\n\nThe second changed `core_pattern` from `core` to `|/data/x`. A core dump is what the kernel collects when a process crashes, and in [Linux's documented pipe form](https://www.kernel.org/doc/html/latest/admin-guide/sysctl/kernel.html#core-pattern) a leading `|` tells the kernel to *launch a program* and pipe the dump into it. We'd already placed a helper at `/data/x`. So the app crashed its own child on purpose, and the kernel dutifully started `/data/x` as **UID 0**. The helper confirmed its identity and capabilities, then put `core_pattern` back. Figure 8 traces that chain end to end.\n\n```\n stock Android starts our /data app\n                  |\n                  v\n Binder/epoll bug yields checked short writes\n                  |\n                  +--> SELinux Enforcing: 1 -> 0\n                  +--> core_pattern: core -> |/data/x\n                                              |\n                                      crash our own child\n                                              |\n                                      kernel runs /data/x as UID 0\n```\n\n*Figure 8. The privilege chain after stock Android starts the installed app.*\n\nThe helper restores `core_pattern` once it's up, but SELinux stays globally Permissive in the demonstrated chain. That tradeoff is real, and I come back to it below.\n\n## Root returns after a cold boot\n\nTo test it for real, I started the Echo the boring way: Action button free, no Kamakiri, no RAM payload. Stock Android launched the app, and the app used the Binder bug to pull root back by itself. In two documented power-cycle tests I left it off for at least 30 seconds before letting it boot with no host attached. The helper returned UID 0 and full capabilities each time. The second of those followed a factory reset and a clean reinstall of the exploit app. I also unplugged USB for a separate power cycle and watched WiFi ADB and the Home Assistant voice setup come back on their own.\n\nAcross **50 consecutive stock software reboots**, a UID-0 command landed over authenticated WiFi ADB on every new boot ID, 50 out of 50. Mean time from reboot command to that check was **150.7 seconds** (range **147.3–154.3 s**), almost all of it Android starting up and getting on the network, not the exploit itself. A factory reset wipes the app and helper, so you'd have to repeat the BootROM stage to set the whole thing up again.\n\n## Making the root useful\n\nThe BootROM flaw is burned into silicon, but the Binder bug isn't. One software update could quietly close the only route that brings root back after a normal boot. So the Echo is configured to start with WiFi off, and once the kernel exploit has root, the helper installs network restrictions *before* bringing WiFi up. In our cold-boot tests, the filters were already in place by the time the device was online.\n\nBut the whole point was the application layer. Kiosk Satellite connects the Plus 2 to Home Assistant, and Music Assistant handles Spotify. Codex and I wired up the physical buttons, the microphone permission, and the LED ring (the stock audio processor even tells us the direction of arrival the ring should show), and our helper restores `RECORD_AUDIO` whenever Fire OS revokes it. All of it runs through the stock audio stack, as we never had to write a single replacement driver.\n\nHere's the finished assistant in use:\n\nWe're still booting Amazon's signed kernel and Android, and LK still checks the boot image. Root lets us reshape the running system, not swap it out. This chain has never booted a replacement kernel or a different distribution.\n\n## Looking back\n\nKamakiri gave powerful but fleeting control at the very start of boot. An installed app gave weak but *permanent* code execution once Android was up. The Binder bug is the bridge between the two: it let that app earn root back on its own, every cold boot. In the USB-free power cycle, the signed stock system and the voice assistant I'd chosen both came back without me touching a thing.\n\nThe build date is the part I still find strange. My unit runs a kernel compiled in October 2026, reporting a March 2026 patch level, and its Binder path is missing the fix from the [October 2019 Android bulletin](https://source.android.com/docs/security/bulletin/2019-10-01). A seven-year-old, publicly documented, once-exploited-in-the-wild bug, alive on an older device running a 2026 vendor build. I don't know why it's still there.\n\nThe two bugs that made this possible, the BootROM flaw and the Binder use-after-free, were found by other researchers. What I actually did was narrower: I chose the device, found and measured its USB pads, and steered the search back onto solid ground when the early-boot path turned into a dead end. Codex did the rest of the hard part. It wrote the RAM bridge, adapted a Pixel exploit to a kernel it had never seen, diffed the captured image against public patches, and helped me stitch the voice stack together. The smart plug and a soldered USB cable gave it something an agent usually doesn't have: a body. It could try an idea, break the device, and try again, all on its own, overnight.\n\nI keep coming back to how much of the reverse engineering it simply *handled*. But it didn't make me redundant. It moved the bottleneck. Someone still had to decide which question to ask next, to notice when a claim didn't match the hardware, to call off the LK hunt before it ate another week of compute. So for now, the scarce skill isn't doing the work. It's understanding the problem well enough to aim the agent at the right part of it. You still need a feel for the system and for the different angles of attack. The upside is that you can build that feel *with* the agent: I learned a lot of this kernel's internals by arguing about them with Codex when I was trying to understand the details. And the thing agents are unreasonably good at is the grind: testing ideas, iterating fast, and making progress while you sleep.\n\nOn cost, I got lucky. I had US$500 of OpenAI compute credit lying around and burned maybe US$200 of it, though I was running other experiments at the same time and can't cleanly separate the bill. I doubt I could have built this exact chain by hand even with much more time. A less directed agent might have stumbled onto the same route eventually, but probably at many times the compute. Giving it a clear goal and a reliable way to test its own ideas is what made it practical.\n\nI'm not going to keep developing this jailbreak forever. I might poke at other old hardware, and I'd love for someone to make the voice integration easier to install and live with. Mostly, though, I just wanted an old speaker to run an assistant I chose, instead of one chosen for me. Now it does.", "url": "https://wpnews.pro/news/rooting-an-echo-plus-2-with-ai", "canonical_source": "https://blog.tripton.dev/rooting-an-echo-plus-2-with-ai/", "published_at": "2026-11-03 14:39:48+00:00", "updated_at": "2026-11-03 14:55:56.393329+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "artificial-intelligence"], "entities": ["Amazon", "Echo Plus 2", "Alexa+", "OpenAI", "Codex", "GPT-6 Sol", "Home Assistant", "MediaTek"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/rooting-an-echo-plus-2-with-ai", "markdown": "https://wpnews.pro/news/rooting-an-echo-plus-2-with-ai.md", "text": "https://wpnews.pro/news/rooting-an-echo-plus-2-with-ai.txt", "jsonld": "https://wpnews.pro/news/rooting-an-echo-plus-2-with-ai.jsonld"}}