Jailbreaking RabbitOS: Uncovering Secret Logs, and GPL Violations
By David Buchanan, 16th July 2024
I assume by now that most people have heard of the Rabbit R1.
Critics unanimously agree that it sucks, and some have accused the company of deliberate deception. Rabbit Inc. reportedly accepts returns, but customers are so eager to get rid of their R1s that even new-in-box units are selling for well below RRP ($200) on secondary markets.
For Sale: Rabbit R1, Never Used
Rabbit community forum member omorneau
aptly summarises (archive) the R1 ownership experience:
I spent 2 hours trying to get my r1 to do anything remotely useful. [...]
I’d sell mine, but honestly I’d feel bad for the person buying it.
I’d give it away, but would feel bad for the person receiving it.
Any ideas?
Member smc
replies:
A jailbreak is being developed [...]
Well, here we are!
In this article I'll outline the boot process of the R1, and how (and why) I subverted it to create a "tethered jailbreak" that gives you a root shell on otherwise-stock firmware, all without unlocking the bootloader or making any persistent changes to internal storage.
I'll also talk about my initial findings from poking around inside the "RabbitOS" firmware.
Motivations
After the headlines caught my attention, I started reverse-engineering a copy of the APK that I found floating around the internet (yes, "RabbitOS" is just an app running in a kiosk-like mode on Android 13 AOSP). There are no "local AI models" or anything like that, so once you understand the API it uses to talk to The Cloud™ you can replace the Rabbit R1 hardware with a small Python script. I reverse-engineered their API, and wrote up my findings (it's nothing very exciting, just JSON over a websocket).
By the way, you might've seen headlines about exposed API keys. Those API keys were allegedly leaked from server-side source code, and were never stored on-device (I can attest to the latter).
A week or so ago I bought an R1 on eBay for £122 (which is still way more than it's objectively worth). So why did I buy this garbage, in full knowledge of its garbage-ness?
Well, in subsequent app updates they started obfuscating their code, and I took it personally! I love a good game of cat and mouse (or tortoise and hare?), and the game was on. What secrets are they trying to hide from me?
They're using a commercial obfuscator, and to be honest it's quite good, making my purely static-analysis approach tedious. So, I decided it was time to get an R1 in-hand, to poke and prod at. Yes, I'd already figured out the API, but I didn't want to get locked out in future updates. Not because I especially care about being able to interrogate Rabbit's mediocre APIs, but because my pride is at stake.
I'd also never looked at the boot security of a modern-ish Android device before, so it was an interesting learning opportunity on that front.
During my static analysis of their obfuscated code, I noted logic to detect off-the-shelf analysis tools like Magisk and Frida (if detected, it'd refuse to run). So, I was probably going to have to develop my own tooling. Fun! Of course, I could try to work around their anti-analysis detections, but that's much less fun. The obfuscated code also takes steps to verify that it's running on an R1, as opposed to any other android device, and I could spoof or patch those checks, but that gets boring (and tends not to be a very future-proof approach).
In other words I prefer to be a reverse engineer, rather than an anti-anti-reverse-engineer.
R1 Hardware
The device uses a MediaTek MT6765 SoC, with 4GB of DRAM, and 128GB(!) of eMMC storage. The SoC is an interesting choice for a newly designed product released in 2024, since it has known bootrom exploits (since 2019!) The 128GB of storage is also a weird choice, since the device doesn't store much locally. Maybe they intended to develop local ML models and gave up. Or maybe it was just surplus stock being sold at a discount.
R1 owners fairly quickly noticed that although the bootloader is "locked" by default, you can use mtkclient to unlock and then reflash it with a "custom ROM" and/or root it. It doesn't even need to use the aforementioned bootrom exploit, because the device is permissively configured. However, I'm not too interested in running a custom Android system image, I'm here because I want a closer look at the factory-installed firmware.
Note: Many are calling the reflashing process a "jailbreak", and I'm not going to argue with them. Just be aware that if you see someone talk about jailbreaking an R1, they might be referring to that.
Although the first boot stages are wide open, subsequent stages implement Android Verified Boot 2.0. I could unlock the bootloader and install Magisk (a root tool which works by patching the boot
partition), but this has several issues:
- It might break OTA delta updates (which, to Rabbit's credit, are regular).
- It might get detected by the current anti-analysis code.
- It might get detected by future updates that check, for example,
ro.boot.verifiedbootstate
(which is set by AVB depending on how happy it is).
All three of these problems are workaround-able, but it'd be so much easier if we could just not cause them in the first place. I want to run as much of the "vanilla" code as possible, with minimally invasive patches to grant me local root privileges, so that I can inspect the app at runtime. The fewer things I change, the fewer things there are for annoying anti-analysis logic to detect.
The solution I came up with was to write a "bootkit" of sorts. Before I tell you how that works, let me explain the default boot process in detail. Fair warning, it's about to get dense.
The Boot Chain
All the boot chain logic comes from MediaTek, the SoC vendor.
The boot process starts in the bootrom (aka brom), which is immutably etched into the CPU silicon, and mapped at physical address 0. The bootrom does very basic hardware initialisation, and then loads the next stage (the "Preloader") from the eMMC boot0 partition, into SRAM. The Preloader is signed, and the bootrom verifies the signature before executing it. (Edit: Actually, on the R1 it might not verify it at all. More research needed...)
The Preloader initialises DRAM, and then loads 3 images from eMMC GPT partitions into DRAM:
tee
Arm Trusted Firmware (EL3)gz
GenieZone Hypervisor (EL2)lk
Little Kernel (EL1)
It verifies their signatures, and then jumps to LK.
Through some process I don't yet fully understand, LK jumps to ATF, which sets itself up and then jumps to GZ, which sets itself up, before returning back to LK, which continues the boot process. I haven't investigated ATF and GZ in much detail so I might be slightly wrong here.
LK is where the interesting stuff happens though. It implements the aforementioned Android Verified Boot, and as part of that, dm-verity, which "provides transparent integrity checking of block devices. dm-verity helps prevent persistent rootkits that can hold onto root privileges and compromise devices. This feature helps Android users be sure when booting a device it is in the same state as when it was last used."
LK loads and verifies the GPT boot
partition (from eMMC userdata, not to be confused with eMMC boot0
), which contains the Linux kernel and initramfs. If the bootloader is in "locked" state, it will refuse to boot if verification fails. If the bootloader is "unlocked" it will still boot, but with a big scary warning saying that the device cannot be trusted, and it also sets various flags to inform the soon-to-be-booted kernel of this (aka "orange state"). If dm-verity checks fail, the device won't boot even if the bootloader is unlocked (It displays a warning and says "press the power button to continue", but it doesn't work. This may be a bug!)
Assuming the requisite checks have passed, LK finally decompresses and boots the Linux kernel, which in turn executes /init
from the initramfs, which in turn mounts the other partitions and does all the other Boot Stuff (which I don't understand too well at present - for my purposes I only need to understand as far as /init
).
By the way, it uses the A/B partitioning scheme (so when I said boot
earlier, that's really either boot_a
or boot_b
depending on which slot is currently active).
Also by the way, bootloader lock/unlock state is stored in the seccfg
GPT partition. The seccfg
data is just a few flags, along with an encrypted hash of that data. The hash is encrypted using the SoC's hardware AES engine, acting as a signature/MAC of sorts. Relatedly, the last byte of the frp
partition governs whether bootloader unlocking is permitted (e.g. via fastboot flashing unlock
, which would update seccfg
on success).
Breaking the Chain of Trust
Secure boot chains all have a root of trust. In this case, the root of trust is a certificate hash baked into the CPU's efuses, along with the bootrom code that verifies it. However, due to the aforementioned "kamakiri" bootrom exploit, the first link of the chain is irrevocably broken. If we can subvert the first stage, we can in principle subvert all subsequent stages, no matter how "secure" they are in isolation. This hardware is fundamentally incapable of hiding secrets from its users (I wish all hardware was like that, to be honest).
But, we don't even need to use an exploit here. Both the brom and Preloader boot stages feature a USB bootloader mode, which in the r1's case will accept unsigned DA ("Download Agent") images over USB, and allow you to execute them from memory (from SRAM in the case of brom, and DRAM in the case of Preloader).
So, I wrote my own DA payload. It gets loaded into DRAM by Preloader and does the following things, in order:
- It loads a custom Android
boot
image over USB into DRAM (containing kernel and initramfs). - It installs a hook in the very last part of Preloader, just before it jumps to LK.
- It jumps back into Preloader to continue the regular boot process.
- Preloader loads verifies the
tee
,gz
andlk
images from eMMC, as it normally would. - Just as Preloader is about to jump to LK, our hook lands, and we take this opportunity to install custom hooks/patches in LK.
- LK continues boot as normal, loading and verifying the original
boot
partition from eMMC. - One of our aforementioned LK hooks is to hook memcpy. When the
boot
image is getting copied from the "AVB" code over to the "boot linux" part of the code (they seem to be separate modules), we substitute in the custom boot image that we initially loaded over USB. - Another LK hook displays a custom message on the screen, just for style points.
- Our custom kernel/initramfs starts booting, while all integrity verification checks pass!
LK uses the MMU to provide memory protection, and although the mappings are all identity mappings (virtual address == physical address), it created some headaches for me. I skipped the details above, but I actually have to copy the boot image around multiple times, as different memory ranges are accessible and/or clobbered at different sub-stages of LK's boot process. There's almost certainly scope to simplify this logic, but hey, it works.
At each stage, my general approach is to let the boot process proceed unmodified, let it verify the data that needs to be verified, and then substitute in my patched data right at the last minute, between verification and use. A bit like this:
Just to reiterate, we don't touch flash storage at any point during this process, the entire "jailbreak" process stays in memory only. This also means that once the device is rebooted it's back to a clean slate, which is often useful when reverse engineering.
For the custom boot image, I used the flashable-android-rootkit project, which is essentially a stripped-down version of Magisk. It replaces the default /init
binary in the intramfs with one that injects a maximally privileged user-space service (the "payload"), before continuing the boot process.
The tool used to do the actual boot image patching, magiskboot
, also comes from Magisk project. It's intended to be executed on-device, but that's not viable in my case because (until we've jailbroken it at least once) there's no way to run our own code on the R1. Fortunately the magiskboot_build project exists, allowing magiskboot to be compiled and executed on regular linux systems.
For my payload, I wrote a quick-and-dirty TCP bind shell - not very "stealthy" (i.e. potentially detectable by the Rabbit app), but I can always improve this down the line.
Since I'm sending a custom boot
image, I could in theory patch the kernel, but I haven't had a need for that yet.
I could also build an entire custom kernel from source, but Rabbit Inc. has chosen to violate the GPL2 license and not make the sources available. Of particular note are their drivers for hall-effect scroll wheel sensing, and camera rotation stepper motor control, which are closed-source and yet statically linked into the GPL'd kernel image. Violations like this are hugely destructive to the free software ecosystem, from which companies like Rabbit Inc. benefit.
Pushing the Payload
I started writing my own USB client software in Python, not because there's anything wrong with mtkclient (which already implements everything necessary) but because I wanted to make sure I understood everything as much as possible. Once I had it working, I decided to port it to js/WebSerial, just for fun.
And now I have a webpage that can jailbreak a physically-connected Rabbit R1: https://retr0.id/stuff/r1_jailbreak/
In the spirit of terrible rabbit-themed puns, I'm naming the jailbreak "carroot".
While booting up, it looks like this:
(Video demo here)
And once it boots, we can log in and have a quick look around:
$ rlwrap nc 192.168.0.69 1337 # id uid=0(root) gid=0(root) groups=0(root) context=u:r:rootkit:s0 # getprop ro.boot.verifiedbootstate green
As you can see, we're root, and the system thinks it's been booted securely, without even needing to tamper with system property values.
Note, my TCP shell is so bare-bones that there's no "#" prompt by default, I added it here for clarity.
The privileged "rootkit" SELinux domain is set up as part of flashable-android-rootkit.
Research Process
In researching the R1's boot chain I benefited from the work of many other researchers and developers who came before me, notably:
- bkerler/mtkclient - Code for manipulating MediaTek devices through the brom/preloader/DA interfaces, and more. Also includes its own links to further learning resources.
- cyrozap/mediatek-lte-baseband-re - Baseband-focused, but also includes hardware/boot notes, and links to further resources.
- 吴港南/preloader运行流程--基于MT6765 ("Preloader operation process - based on MT6765") - contains some helpful diagrams and MT6765-specific notes.
- ng-dst/flashable-android-rootkit, LuigiVampa92/unlocked-bootloader-backdoor-demo, topjohnwu/Magisk - These projects and their associated documentation cover the later stages of the boot process, from
/init
onwards. - RabbitHoleEscapeR1/r1_escape - Tools/instructions for flashing "custom ROMs" on the R1.
If you look at the iFixit teardown photos, you can see test pads labelled TX and RX. These are of course UART test pads, which were invaluable during my research. The logic levels are 1v8, although they appear to be 3v3 tolerant (at least, 3v3 did not blow mine up). At all stages of the boot chain, the device logs debug information over UART (at 115200 baud during brom, and 921600 baud thereafter).
The Preloader has an annoying feature that disables UART logging unless the volume-up key is being held. The R1 doesn't have a volume-up key, so I had to patch preloader to disable this check (and I can boot a patched Preloader using the bootrom's USB download mode).
I was also able to patch the Linux kernel's commandline flags to emit kernel logs to UART, like so:
earlycon console=ttyS1,921600
These combined patches allowed me to gather logs for the whole boot process.
During development of my jailbreak tool, I was able to emit UART logs from my own code, for "printf debugging".
P.S. I think I spy an unpopulated JTAG header, which I have not yet investigated further.
P.P.S. The test pad next to the reset button (accessible through the SIM slot, closest to the edge of the board), can be pulled to ground during reset to force the device to boot into brom's USB mode.
Findings
So, what were they trying to hide from us?
To be honest I haven't found anything particularly interesting yet. The analysis has only just begun! A big reason why I'm sharing my jailbreak is the hope that other people will join me in my analysis.
One thing I did notice is that they were logging everything to text files on internal storage:
:/storage/emulated/0 # ls -al ./Android/data/tech.rabbit.r1launcher.r1/files/logs/ total 7140 drwxrws--- 2 u0_a66 ext_data_rw 4096 2024-07-07 00:52 . drwxrws--- 3 u0_a66 ext_data_rw 4096 2024-07-04 22:11 .. -rw-rw---- 1 u0_a66 ext_data_rw 671954 2024-07-05 01:37 2024-07-01.log -rw-rw---- 1 u0_a66 ext_data_rw 1472020 2024-07-04 23:40 2024-07-04.log -rw-rw---- 1 u0_a66 ext_data_rw 782800 2024-07-06 16:45 2024-07-05.log -rw-rw---- 1 u0_a66 ext_data_rw 1747449 2024-07-07 00:52 2024-07-06.log -rw-rw---- 1 u0_a66 ext_data_rw 2565224 2024-07-07 03:47 2024-07-07.log
At the time (July 7th), I noted this publicly in the Rabbitude community discord. I just thought it was funny that they were choosing to fill up their 128GB of storage space with such verbose logging.
But as I and others looked closer, and thought about it more deeply, things became concerning.
These logs include:
- Your precise GPS locations (which are also sent to their servers).
- Your WiFi network name.
- The IDs of nearby cell towers (even with no SIM card inserted, also sent to their servers).
- Your internet-facing IP address.
- The user token used by the device to authenticate with Rabbit's back-end API.
- Base64-encoded MP3s of everything the Rabbit has ever spoken to you (and the text transcript thereof).
This was concerning because:
There's simply no need to be logging this much data in this much detail, especially on a device with no meaningful hardware security. I dread to think what they're logging on the back-end side of things, too!
There was no end-user-facing way of factory-resetting the device, making the logs effectively permanent. Combine that with the highly active second-hand markets and you have a recipe for disaster. There isn't even a "log out" button on-device!
Fortunately for whoever I bought my R1 from, I factory reset it using mtkclient
before doing anything with it.
Fortunately for everyone else, the latest RabbitOS update (v0.8.112) addressed this issue while I was midway through writing this article. They reduced logging and added a factory reset settings option!
Credit where it's due, this was a very quick turnaround, and it's the first time I've seen Rabbit being even slightly proactive with regards to user privacy and security issues, as opposed to merely reacting to news articles written about them.
I didn't bother reporting it as a security issue myself, firstly because I hadn't fully thought through the impacts at the time, and secondly because I didn't expect to be taken seriously by the company, based on their prior responses to security issues. I was also preoccupied with working on my jailbreak tool! I guess someone else reported it to them though, and I must admit I was positively surprised by their response. I hope this marks a step in the right direction in their attitude towards security issues. If anything, they made the issue sound more serious than it actually is, rather than trying to downplay it.
It would of course be better if they never logged this stuff in the first place, but it's all part of the "move fast and spray PII everywhere" mindset we've come to expect from the modern tech industry.
This reaffirms my belief that consumers should have full ability to inspect and modify the code that runs on the devices they own. When these abilities are denied by a vendor, I take steps to rectify the situation.
AOSP "Customizations"
When the news broke that the RabbitOS was just an app running on Android 13, Rabbit's PR spin on the matter was to call it a "very bespoke AOSP [with] lower level firmware modifications".
So, what are these bespoke modifications?
As I said before, my analysis has only just begun. But as far as I can tell, all they've done is disable any and all Android features that could compromise their single-app kiosk mode experience. For example, there is no navbar, no notification bar, etc. They've even taken (ineffective) steps to prevent people from enabling ADB (There's an app, named "Judy", that runs in the background with the sole purpose of disabling ADB if it's found to be running).
On July 4th, @MarcelD505 shared a very clever "kiosk escape" trick (mirror), starting from the wifi captive-portal-login web browser, and ending in the Android system settings app, where various useful changes could be made (not shown in the demo video, but you can also use the Android accessibility settings to adjust the system font sizes, for example).
Rabbit "fixed" this in the latest update by removing the Android system settings app from the device entirely. I'm sure they'll say they did this to "improve security", or something.
If your product's security relies on denying users access to their own system settings, you fucked up somewhere along the line.
So yes, it is a rather bespoke AOSP, but all I've seen thus far are attempts to subtract existing functionality.
The thing that really gets my goat is that there's so much they could be doing. A legitimate reason for not being "just an app" is that regular apps on regular phones have to run within Google or Apple's walled gardens, limiting potentially useful integrations. For example, a regular app doesn't have permission to initiate a phone call and pipe procedurally generated audio into it. But at present, neither does RabbitOS. It's not even something on their already-overambitious roadmap.
I have yet to see any compelling technical reasons that RabbitOS as-implemented can't just be a regular app on a regular phone.
Maybe you like the idea of a dedicated device for a single task. Like the "iPod Classic" of AI assistants. Just say that then! There's no need to make up bullshit technobabble excuses.
Advice for Regular R1 Users
If you're worried that your device may have been "jailbroken" against your will, just turn it off and on again. If it boots up normally without any warning messages on the screen, you're probably safe. (Edit: unless brom is configured to allow booting unsigned Preloader images from eMMC, in which case you still can't be sure - I'll test this soon)
Ideally you'd be able to reflash known-safe stock firmware using vendor-provided firmware images, but the vendor does not make such provisions. You should ask them for it!
Never leave your R1 unattended, because any data stored on it could easily be extracted by someone who knows what they're doing.
If you're planning on selling (or donating, or trashing) your R1 device, you should factory reset it first using the newly added settings option.
Conclusions
So to wrap things up:
- The Rabbit R1 has no special hardware: aside from the scroll wheel and rotating camera, it is a generic MediaTek Android device.
- Their AOSP "customizations" mostly consist of removing features to better enforce a single-app kiosk mode.
- The Rabbit R1 has ineffective boot-chain security, meaning you can't safely leave your device unattended.
- Rabbit Inc. is violating the Linux kernel's GPL license.
- None of the above bullet-points are new revelations by me, I'm just spelling them out more clearly.
- I'm releasing an experimental Tethered Jailbreak tool, to assist researchers in getting access to their own R1s, and eventually letting advanced users extend the device's functionality.
- It was discovered that the R1 logs excessive user information to internal storage, in a way that was impossible for regular users to erase. Rabbit Inc. swiftly rectified this prior to publication of this article. They can't fix the known-since-2019 bootrom issues though, since the bootrom is immutable.
Addendum
On July 12th, I asked Rabbit Inc. if they had any comments to make on the content of this article, along with explicitly asking them if they had plans for compliance with the GPL license (and I know I'm not the first to ask the latter).
This article does not constitute a security disclosure (I am not raising any new security issues here), but I thought it would be fair to give them an opportunity to make a statement nonetheless, especially with regards to GPL compliance.
As of July 22nd, they have not responded.