[STORYTIME] I brought my bootlooped iPhones back without restoring or updating

thehermeticdev

Regular Member
Jr. VIP
Joined
Sep 17, 2025
Messages
304
Reaction score
148
Hermetic here.

A few days ago I killed one of my favorite test phones while doing kernel experiments.

The device would reach the Apple logo, crash, reboot, and repeat forever.

Normally this is where you connect the phone to a computer, restore the IPSW, and move on with your life.

Except there was one problem:

I absolutely could not restore it.

The phone was sitting on an iOS version I specifically needed for my research. Apple no longer signed the version I wanted to preserve, so a normal restore would have forced an update and permanently removed the exploit compatibility I was keeping the device for.

The data also mattered.

So the objective became:

Fix the phone without restoring it, updating it, or touching the existing data any more than necessary.

And apparently the solution was a $4 microcontroller.


Going underneath iOS​

The setup was based around an RP2350 board and usbliter8, which targets the A12 USB/BootROM path.

The important part here is that this happens below iOS.

From DFU mode, the board communicates with the device over USB and puts it into a state where I can load my own boot environment.

From there I was able to work through a patched boot chain and eventually load an SSH ramdisk entirely into RAM.

That distinction matters:

the rescue environment isn't installed onto the phone.

It gets pushed into memory over USB, boots temporarily, and gives me enough access to inspect and repair the filesystem.

No IPSW restore.

No version upgrade.

No wiping the phone.

The existing iOS install and user data stay where they are.

At least, that was the theory.


The first night was a war​

Getting it working was significantly less elegant than explaining it afterward.

I tried basically everything.

There were failed boot attempts, inconsistent USB behavior, a hub that disconnected whenever it felt like it, and ramdisk sessions that seemed determined to expire exactly when I finally had everything mounted.

At one point I was also fighting a data volume that didn't want to create new files but was perfectly happy letting me overwrite existing ones.

The RP2350's LED indicators also managed to convince me several times that something had succeeded when it absolutely had not.

After enough attempts, though, I finally got a stable rescue environment.

SSH worked.

Filesystem mounted.

Phone was alive enough for surgery.


First rule: read the crash before fixing anything​

Before changing anything, I wanted to know why it had died.

If you don't understand what caused a bootloop, blindly deleting files until the phone starts again is just asking to reproduce the same problem later.

So the first objective was retrieving the relevant panic/crash information and tracing the failure back to whatever I had changed during my kernel/tool experiments.

And eventually I found it.

The cause was unbelievably stupid.


The actual bug​

One line in one of my tools writes a system preference.

That preference is supposed to contain a boolean value:

basically:

true / false

or:

yes / no

Instead, because of a bug in my code, I had written an array.

The modification itself didn't immediately kill the phone.

Everything looked normal.

Until the first reboot.

During startup, SpringBoard reads the stored preference and expects a boolean-like object.

Instead it receives an array.

Something in that path then effectively tries to interpret the array as a boolean.

SpringBoard dies.

The system retries.

SpringBoard reads the same corrupted preference again.

It dies again.

Repeat forever.

Permanent bootloop.

And because the bug only manifests after rebooting, it's exactly the kind of mistake that's extremely easy to miss while developing.

Even better:

I had done this to three devices.

Two of mine and one belonging to my partner.

Excellent QA process.


The fix was almost embarrassing​

Once I had filesystem access from the SSH ramdisk, the actual repair was tiny.

Mount the relevant volume.

Locate the poisoned preference.

Replace the incorrectly typed value with the value it should have contained.

Reboot.

Apple logo.

Wait.

Lock screen.

The phone came back like absolutely nothing had happened.

Same iOS version.

Same apps.

Same data.

Same files.

No restore.

No update.

Everything exactly where I had left it.

After roughly three days of fighting the recovery process, the actual software fix ended up being one bad value.

Classic.


Then I did the same surgery on the other two​

Once the first phone was fixed, the mystery was gone.

The remaining two devices had the same root cause, so the process became much more straightforward:

boot the rescue environment, SSH in, mount the filesystem, fix the malformed preference, reboot.

Both came back.

So the final score is:

3 zombie devices recovered.

Two of mine and one belonging to my partner.

No restores.

No forced updates.

No lost data.

And most importantly, the iOS versions I wanted to preserve stayed exactly where they were.

The source bug is fixed too, so that specific landmine is dead now.


Devices​

The setup involved iPhone XR devices running:

  • iOS 18.7.1 (22H31)
  • iOS 18.6.2
Preserving those installed versions was the entire reason I went through this instead of just restoring normally.

The little RP2350 board did exactly what I bought it for.

It is still kind of absurd to me that a cheap microcontroller sitting on my desk can participate in a recovery chain like this.


Resources / setup​

Leaving this here because it may save someone else some research if they're experimenting with A12 bootROM tooling or trying to understand a recoverable bootloop.

Repositories​

usbliter8
https://github.com/prdgmshift/usbliter8

ICH_A12_plus_Ramdisk
https://github.com/Pa7r0n/ICH_A12_plus_Ramdisk

pico-sdk
https://github.com/raspberrypi/pico-sdk

I pinned the Pico SDK to 2.2.0 because the exploit path is timing-sensitive.

ARM GCC toolchain used:

https://github.com/xpack-dev-tools/...-none-eabi-gcc-15.2.1-1.1-darwin-arm64.tar.gz

Tooling / libraries​

  • CMake
  • Ninja
  • libimobiledevice
  • pyserial
  • pyimg4
  • capstone
pyimg4 and capstone were pulled automatically by the ICH setup script in my case.

Hardware​

  • Waveshare RP2350 USB-A board
  • USB-A → Lightning cable
  • USB hub
  • iPhone XR
  • a questionable amount of patience
One particularly nice part of the ramdisk tooling is that you don't necessarily need to keep a giant archive of IPSWs locally.

The builder can pull the specific components it needs from Apple's servers instead of downloading an entire firmware image just to extract a few files.


Anyway:

three zombies in, three zombies out.

All recovered.

And the funniest part is that after all the BootROM exploitation, patched boot chains, USB problems, SSH ramdisks, and filesystem surgery…

the thing that actually killed all three phones was basically:

wrong variable type.
 
Back
Top