Skip to content

Предоставляется на условиях «как есть», без каких-либо гарантий. ЦНИИ Башня Тиробрии не несёт ответственности за любой ущерб, возникший в результате использования данного инструмента. Использование осуществляется на свой страх и риск.

PROVIDED AS-IS, WITHOUT WARRANTY. TSNII-BT BEARS NO RESPONSIBILITY FOR ANY DAMAGE ARISING FROM THE USE OF THIS INSTRUMENT. USE AT YOUR OWN RISK.

終焉に微笑むメシア.

「Putting too much faith on Google Security.」

No header offset. The longjing manifest is a binary struct at a fixed position, not a key-value section. The `security_version` field lives in the manifest instead of an antirollback counter. No rec frame count does not do framed rescue payloads. The timestamp is 0. thing ships in Pixel 11, and this image was built before its release, so the firmware carries no build date. The RW payload is 344076 bytes, a third larger than the d3m2's 258048. The manifest offset is 65536, exactly half the Dauntless position .

RECOVERING
A TITAN.

This sequence is a ritual. Not because the chip requires ceremony. Because the engineer needs structure against the silence. The chip will never explain itself. The sequence is the only meaning the engineer can impose on a component that refuses to communicate. Every check is a rebellion against the irrational.

yes dauntless secure attack resistant truly

That’s the hardware module chip
It’s way too secure than tee
It doesn’t share your cpu
your memory, and has its own storage
it’s both sw&hw resistant against attack
even if you were a hw hacker you woudn’t be able to break in

i definitely not revising this 67 times just to say 67

This page is not designed to be read on mobile by design, I’m tweaking while working on this and so will you, so bear with me.

RW firmware is the single least understood piece of the whole Pixel stack. Most people only meet it the day an ordinary flash fails to explain why a phone with a working bootloader simply wont boot. The device is COOKED. No warning. No tutorial. Just a brick, and nobody at the vendor will tell you what the fuck happened.

Firmware rescue means opening the funny website, plugging in the phone, and doing whatever it tells you once it notices GSC RW firmware is dead. The bootloader still responds. The screen reads Secure boot: PRODUCTION. The phone is technically still speaking. Its lowkey lying to you.

This is fine. Everything is fine.

Meanwhile, another line says NOS production: error!. NOS is Nugget OS, the chip’s own firmware. The active GSC RW firmware partition is corrupt. The bootloader can answer. The hardware trust root can’t.
This is the part where the NPC at that sold you the phone quietly looks away.

You want to write the firmware and go home. GSC rescue flow has other plans. Inspect the chip, build a plan, execute it, then check what actually came back. Writing is one part of that sequence.

Every rescue frame carries a SHA-256 check. A mismatched frame gets no ack and the stream stalls right there. Resend it clean and the run continues. Give up and the rescue is dead. Even a successful command can leave the partition state wrong. The command says gg. Perlica asks for receipts.

Meet Perlica, our sussy wifey. Shes a Rust CLI for inspecting Dauntless rescue records and Epic/长晶 RW payloads. She turns the protocol into an inspect, plan, execute, and verify sequence. We ask her to save the phone. She asks for the partition state. Romance is going well.

meet sussy wifey meme collage: Perlica screenshots with notes about her Endfield loyalty and Originium documents
Rust CLI for inspecting Dauntless rescue records and Epic/长晶 RW payloads that we working on.
North Wuling city under a fractured purple sky

north wuling exclusion zone

Every security chip has one component that the phone trusts more than it trusts itself. GSC is that component. It signs. It attests. It decides whether the boot chain is intact. Final boss of the trust chain. Doesn't drop loot. Also the root of trust, and nobody is getting root on it. You can't grind it. You can't parry it. You can only ask it nicely, over SPI, and hope.

Hope is the only lubricant this interface ships with. The pull rate on a working phone is not posted.

It's also the component whose rescue protocol is documented for googlers and kept from everyone else.

Not "poorly documented." Not "lost." Just on the wrong side of a wall. The protocol lives in the bootloader and the flasher that drives it, nowhere a user is meant to look. Everyone outside the wall is an NPC running on guesswork. The vendor treats the protocol like a raid crew treats a rival MMO. You are here to use the phone. Reading the firmware is grounds for removal. This isn’t up for debate. Tread CAREFULLY.

The GSC talks through a set of oem commands that aren’t advertised. The commands exist in the bootloader firmware. They don’t appear in any help text. You find them by reading the binary or following protocol traces. This is the part where

would skip the inspection, Aether-edit the bytes straight in, and then act surprised when the device dies. We aren’t doing that.

Ask for an explanation and you get partition markers. It doesn't do small talk. Perlica parses them and takes notes.

Ask the chip for its state first:

fastboot oem gsc version

Rescue starts by asking for that report. Perlica does the same, then asks for each slot’s header to fill in the details. The partition table returns ok or it doesn’t, and when it doesn’t, there is no error code. Just silence. The chip saw your flash and said jack shit. The markers tell the host what it can see. Perlica builds a plan from that report.

  • fastboot oem gsc: checks if GSC responds (10s timeout)
  • fastboot oem "gsc header RO_A": per-partition header details
  • fastboot oem gsc sjtag-cfg: returns "0" or "1"
  • fastboot flash gsc <firmware.bin>: flash a full GSC firmware image
  • fastboot oem gsc rescue: execute firmware rescue

Unless you are a Googler with sjtag approved device you are not seeing 1 on this

GSC exposes a logical A/B interface through RO_A, RO_B, RW_A, and RW_B. An absent row isn’t a missing bank. Its the banner slot with nothing pulled, no version, no hash, valid and selected both false. After Dauntless rescue, that empty record is bad news. If RW_A is absent, unselected, or missing its ok, GSC acceptance check fails. The rescue state is still degraded.

A selected RW with no parsed version is also degraded at preflight. If both RW versions are unavailable, the selected RW hits that same wall, and the reference flow doesn't mince words: rescue-needed, firmware may be corrupted or outdated, pure rescue.

Neither RW selected. Both default records are unselected. No pull, no 50/50 to lose, same route anyway. Both RW rows unreported. Same route again.

One or both RO rows missing does nothing to the normal route or the post-operation checks. This isn't the same thing as a completely unavailable inspection report. Don't confuse them. The chip can answer and still be cooked.

/spil_ is literal text in the host-visible version report, not a decoded state the parser guessed at. The reference flow calls this Upgrading from SPIL and treats it as very old GSC RO firmware, so old it needs an intermediate image and the pure-rescue route to even talk to the current chip. The marker can coexist with a selected RW row. It isn’t inferred from missing RW fields.

Post-flash validation requires both RW slots reported valid. An omitted RW row fails the exact same predicate as an invalid row. Thats a failed flash validation, period.

How the implementation uses the report

Perlica follows the reference rescue flow, using the initial report to choose a route. It doesn't have to pass the checks that run after the plan executes. The report gets you started. The checks are what let you leave with a working phone. For Dauntless, /spil_, no selected RW reported, or no selected RW version selects the pure-rescue branch. Otherwise a compatible full image can select flash-then-rescue. After flash or rescue, the flow asks for a fresh report. This is an implementation of that flow.

Both the reference flow and this project require valid status for RW_A and RW_B after a full flash. Dauntless post-rescue acceptance is RW_A-specific, and this is the part people get wrong. RW_A must be selected and valid, and when an expected hash exists its hash must match. A selected and valid RW_B alone doesn't satisfy that check, no matter how proud of it you are. GSC does not give a shit about RW_B pride.

Wuling metro tunnel rendering of when-the-wire-is-80-1m-80-0m

An RW_A missing at the start routes to rescue. An RW_A still missing after rescue fails acceptance. The rescue didn't save you. Don't declare victory. You are on guarantee now. The next attempt comes home as RW_A or it doesn't come home.

Perlica recognizes RO_A, RO_B, RW_A, and RW_B when present, and recognition isn't the same thing as a validation predicate. The tested Epic report has no RO_B row. Epic takes rescue-only, so the RO row being there or not doesn't change its route. For an unknown Epic revision the reference flow falls back to an RO-header query to identify the revision, and if the header metadata is missing that stops identification cold with Unable to get key id from header. Thats an identification failure. Calling it an RO-health state would be cope.

// this is what discovering the rescue protocol looks like
// 1056 bytes, just hashes and offsets. no file magic, no prefix.
// you learn this by reading the parser.
let flash_offset: u32 = 0x20000; // destination offset for this frame's payload
let sequence: u32 = 0; // final frame = bit 31 set (0x80000000)

let rec_frame = RecFrame {
    frame_hash: Sha256::digest(&[...]).into(), // first 32 bytes
    sequence: sequence.to_le_bytes(),
    offset: flash_offset.to_le_bytes(),
    payload: [0u8; 984], // payload data
    ack_hash: Sha256::digest(&[...]).into(), // then rewrite every 0x00 byte to 0x01
};

Each frame is 1056 bytes flat. This is frame data, literally, and the chip doesn't take any of it on faith. 32 bytes frame hash, 4 bytes sequence number (little-endian, bit 31 signals final frame), 4 bytes offset, 984 bytes payload data, 32 bytes acknowledgement hash. The acknowledgement hash is a second local SHA-256 over the record fields, with every 0x00 output byte rewritten to 0x01. Each frame is verified against its own SHA-256. 263 frames in the d3m2 image, one frame at a time, no batching, and the chip acks each one before the next goes in. One bad hash and the chip just stops acking that frame. It never says which byte was wrong. Silence is the whole error message. Resend the frame clean and the stream moves again.

Domain Expansion title card artwork

dauntless valley

Both chips check firmware versions, but they disagree on which direction is bad. You wanted to update a phone. You now need to identify which security chip considers progress a threat. Perlica compares the image's version state with the device's and refuses a payload the chip will reject.

长晶 uses the rule you expect. Per the OpenTitan manifest, security_version is a monotonically increasing counter used for anti-rollback protection. It only goes up. Older firmware takes the L. Perlica refuses an image below the device's security_version.

Dauntless carries no counter. Its rollback state is a 16-byte infomap bitmap, a burn-count measured in strikes, and Perlica reads the bits. It sounds backwards. The recovered firmware prints blob version $ != $ to accept a blob by version match, and ALSO runs a forward gate that prints I key $ requires upgrade: blob version $ -> $ to reject a blob AHEAD of the device.

Perlica mirrors that forward gate. It refuses an image ahead of the device, not behind it. Anti-powercreep, in silicon. You pull the newer firmware. Dauntless refuses it. You explain that its an upgrade. Dauntless refuses it. Congratulations on your download. だめでした。

This was backwards in an earlier draft of this very tool. Perlica's own error is still named DowngradeBlocked. We read the firmware, identified the direction of the check, and implemented the other direction. The error name survived. Our credibility did not.

We built the check to stop you getting this backwards. We were the first customer.

まずい。

Classic antirollback isn't in the analyzable firmware. If the chip enforces it, that lives in the unrecovered boot ROM. A strike count below the per-revision minimum is only a soft warning.

Different field, different direction, not the same rule. Read the gate before you copy the check. Especially if you're copying ours.

b hnjkl,.cfb hnjAWSXDbhjkmn,.hnjmkASXDHIJKLjofhnawsdrghjnouvrf huowdrf f jikkkkkkkkMN.ASDCV BJKLMN. I don't wanna be here I don't wanna be here
my keyboard is slightly tweaking while I'm writing this and this is hilarious so I kept this section intact.

Grayscale key art: a ruined industrial structure tilting under falling debris

the loadout

Perlica has some neat features, which are

perlica inspect-image fixtures/d3m2.ec.bin
perlica --serial <SERIAL> inspect-device
perlica --serial <SERIAL> rescue --rescue-image fixtures/d3m2.ec.bin --dry-run
perlica --serial <SERIAL> rescue --rescue-image fixtures/d3m2.ec.bin --yes

inspect-device runs fastboot oem gsc version for partition state, then attempts oem gsc header RO_A, RO_B, RW_A, RW_B for per-slot header details (header failures are ignored). Perlica also provides extract-rw --input --output for extracting RW payloads from full images and multi-retry --image-dir for scanning candidate images when the chip revision is unknown.

Its rerolling, but the account is a directory of funny binaries. No spark in the shop. The chip is allowed to be shy. Very reassuring in a security product.

Only the last line writes. It requires --yes instead of -y because a destructive command should look different from the commands that came before it. You don't put the nuke on the same key as the flashlight. No -y. You spell out the whole word before anything goes in.

The many routes to rescue. Flowchart: the input is the gsc version plus image metadata, then the target family decides the route. Dauntless (target 0) takes flash-then-rescue only when it has an active RW with a parseable version, no literal /spil_ marker, ARB okay, and a flashable full image available; policy pass, or mismatch as a warning. Flash-then-rescue flashes the full image, verifies both RW, stages the .rec, then runs rescue, and the post-flash version tuple must match. If any check is false, Dauntless takes rescue-only. Epic (target 1) always takes rescue-only: a raw RW payload with no full flash, then stage, rescue, and re-inspect; the post-rescue hash must match. Rescue-only stages a compatible payload, runs rescue, and re-inspects, for Dauntless or Epic. Red stop states: no compatible image, no flashable full image, stage or rescue transport failure, post-flash verify failure, post-rescue verify failure. --allow-policy-mismatch can let an amber warning through; you still need --yes.

the route selector

Perlica validates the local image format, target family, proto version, timestamp, hash, and policy fields. It asks the connected device for its GSC version, chip family, slot state, adn rollback state through the oem commands. It builds a plan and prints every step. Only then does it wait for you to commit. The only branch protection is --yes. "Oops all bricks" isn't a feature.

The planner checks:

  • Target compatibility between the image and the device
  • Whether a selected RW exists and carries a parseable version, or the device is in a /spil_ state
  • Antirollback checks (Dauntless) and security-version downgrade checks (Epic)
  • Whether the path is rescue-only or flash-then-rescue

Before anything writes, Perlica shows which path she picked and what needs to be true before it can run. Spend three days on the door before you reinstall the software.

Dauntless can go either way. If the device meets the route conditions, it can flash a full GSC firmware image, check the state afterward, then stage rescue. A device with no selected RW carrying a parseable version falls back to rescue-only. A /spil_ report selects the intermediate-image branch of that rescue path.

长晶 always goes rescue-only.

perlica make-rec --input fixtures/d3m2.ec.bin --output /tmp/d3m2.ec.rec

make-rec is dauntless-only by design. Format isn't a suggestion. The chip enforces it in silence. You find out by failing.

Cartoon of an operator collapsed on the grass at the end of a taut tether, a distance readout of 80.1 against an 80.0 m limit, watched from behind by a pale-haired figure

choose your rescue shape

Two supported firmware-image families. Dauntless and Epic (长晶). They rescue differently.

Dauntless is the small one. Epic is the normal-sized one.

Im not finishing that joke.

The Haven family, target 0, is the older line. Dauntless, and the Titan M chips before it. That older generation went by Citadel. The whole family sits under GSC name, Google Security Chip. Proprietary, every generation. The wall around the rescue protocol never moved. Its rescue is a framed ritual the chip enforces in silence, one you work out by reading the binary or reversing the flasher. The vendor documented it for insiders. Everyone else gets to guess. Honestly? Kind of based of them.

长晶, target 1, is the OpenTitan line. Epic, Titan M3, the newer chips, built on a project whose architecture is public. Its rescue is a raw RW slice declared by a manifest. No framing. No ceremony. Simpler in shape, though the manifest layout was worked out from the images the same way, not handed over in any spec.

Each rescue input format needs its own parser. Neither chip ships a document for the person holding the brick. The difference is the shape of the path. One is a gauntlet of hash-checked frames, the other is a single staged slice. Epic runs OpenTitan's OTBN crypto engine. Dauntless doesn't. Dauntless does its math on a custom bignum peripheral, not an OTBN core. Different lineage, different silicon. Perlica still refuses to cross the families. Try to rescue a Haven device with a 长晶 image and it stops cold with "长晶 is not part of the Haven family of chips." I've portrayed the Haven device as the based one and the 长晶 image as the cringe one. Perlica agrees. Skill issue.

Both chips sit behind the same wall. The vendor's flasher speaks both dialects. Nobody outside speaks either. Perlica is the outsider that learned both anyway.

the actual sequence

Nobody gets an explanation for how the chip ended up here. It just shows up COOKED and expects you to figure out the lore. The partition table reports selected and valid slots, not causes. The sequence turns those markers into a candidate rescue plan.

Dauntless takes the longer branch:

  1. Inspect: fastboot oem gsc version, then oem gsc header RO_A, RO_B, RW_A, RW_B
  2. fastboot flash gsc <full-image>
  3. Inspect again, then verify both RW are valid and the selected version matches the flashed image
  4. fastboot stage <rescue payload> (a full BIN is converted to a temporary .rec first, a supplied .rec is staged as-is)
  5. fastboot oem gsc rescue
  6. Inspect a third time, then verify RW_A is selected and valid and its hash matches the staged image

长晶 (Epic/Titan M3) is the easier raid. we takes the rescue-only path for it, with no flash and no post-flash inspection.

  1. Inspect: fastboot oem gsc version, then oem gsc header RO_A, RO_B, RW_A, RW_B
  2. fastboot stage <raw RW payload> (a full BIN is reduced to its manifest-declared RW slice first, a supplied RW payload stages as-is)
  3. fastboot oem gsc rescue
  4. Inspect again, then verify RW_A is selected and valid and its hash matches the staged payload

Two inspections. One stage. One rescue. No .rec framing, no /spil_ intermediate path. Cleaner run.

Each phase gives the host another state to record. The chip grunts. Perlica writes it down. Don't read too much into it.

A rescue command can complete without error while the post-rescue state remains unacceptable. For Dauntless, Perlica inspects a third time and doesn't declare success until RW_A is selected and valid and its hash matches the staged image.

The chip calls gg. Perlica asks it to show its work.

For Dauntless, that RW_A state and hash match are GSC rescue acceptance condition. Hard pity. The run ends on a match or it doesn't end. No match, no phone, tough shit.

(its two in the morning and Im explaining a rescue protocol to a chip that doesn't know I exist. this is normal. this is fine. I should sleep.)

the hidden branch

An intermediate image gets its own branch in the reference flow. Hidden route unlocked. A sequence break in the recovery flow.

A literal /spil_ marker in the raw Dauntless version report selects it. The flow then chooses an intermediate rescue image compatible with the target and revision, stages it, and continues with rescue. Thats a branch in the host recovery flow.

Chip revision still decides which image is compatible, but only within the image role already chosen. This project also displays an intermediate_eligible_revision annotation. That annotation isn't an independent device state and doesn't select this branch. Reading the label as the state is the classic NPC move.

If theres no compatible intermediate image, the plan stops. It doesn't fall through to a target image and pray GSC accepts it. Perlica isn't picking the next file alphabetically and hoping for character development.

Frog and Toad meme: Frog puts the UEFI app signatures in a Secure Boot so they will not boot any more unsigned UEFI apps.

what the tool is actually selling

The funny binary tells a story without saying anything. I've been staring at it longer than I've stared at most of my relationships. Anyway. しょうがない。

header offset:      131072
RW header count:    3
RW length:          258048
version:            0.2.2
timestamp:          1742253878
antirollback:       126
rescue payload len: 258792
rec frame count:    263

Then the funny binary starts talking. The nugget firmware prints its errors in two words, lowercase, no article. allocate sps memory fail. load keys fail. A typo shipped to production silicon: Maybe invalide os_ver:. The chip fakes its own reboots and announces Fake reboot. When it really gives up it prints ***HANGUP-FAIL*** ***HANGUP-FAIL***, twice.

This is the component your phone trusts more than it trusts itself, and it can't spell.

the dauntless RW header

Dauntless doesn't use the OpenTitan manifest below. Its RW header begins with the little-endian word 0xfffffffd. The parser scans candidate headers on a 0x4000 stride. The magic word is the only thing in this header that isn't a hash.

+0x000  magic                    0xfffffffd
+0x328  rw_length
+0x32c  flash_offset
+0x330  end_a
+0x334  expected_header
+0x338  end_b
+0x34c  infomap                  16 bytes
+0x35c  generation
+0x360  major
+0x364  minor
+0x368  timestamp                8 bytes
+0x418  hash string              128 bytes

rw_length, flash_offset, and the version fields select a usable image. infomap is the Dauntless rollback-state bitmap, not 长晶's scalar security_version. The end_a, expected_header, and end_b fields are structural checks. Perlica doesn't pretend to know their deeper chip-side semantics. It isn't going to lore-drop fields it hasn't verified.

Inside the funny binary are three structurally valid headers, at 0x20000, 0x80000, and 0xa0000. The outer two are identical real builds. The middle one, 0x80000, is an older build just sitting between them, empty hash, antirollback 0. The short output above selects the 0x20000 header. These are header locations, not proven A/B slot labels.

header     rw length  flash offset  version  timestamp   antirollback  hash
0x20000    0x3f000    0xa0000       0.2.2    1742253878  126           nugget_v0.0.ab13227640-f475af9-r
0x80000    0x17000    0x100000      0.2.114  1736959094  0             empty
0xa0000    0x3f000    0x120000      0.2.2    1742253879  126           nugget_v0.0.ab13227640-f475af9-r

the Dauntless .rec rescue file

A Dauntless rescue file has no file-level prefix, manifest, or trailer. No intro cinematic. Just fixed-size 1056-byte frames, and the file ends at the first frame whose sequence has bit 31 set. Roll credits. When the rescue actually runs, the firmware prints SPI rescue! like its late for a train.

+0x000  frame_hash       32 bytes  SHA-256(sequence || flash_offset || payload)
+0x020  sequence          4 bytes  high bit (0x80000000) marks the final frame
+0x024  flash_offset      4 bytes  destination offset for this payload
+0x028  payload         984 bytes
+0x400  ack_hash         32 bytes  SHA-256(frame_hash || sequence || flash_offset || payload)
+                                     with every 0x00 byte rewritten to 0x01
+0x420                         end of frame (1056 bytes)

The hashes cover the fields shown, not the whole file. flash_offset means frames need not be contiguous. Perlica's decoder fills gaps between supplied ranges with 0xff, because a lazy planner pads with ones and asks no questions. The final record has the same layout as every other frame. It only sets the high bit in sequence.

Count the d3m2 rescue file and you get exactly 263 frames with no trailing bytes. Tight.

file size        0x43ce0 (277728 bytes)
frame count      263
frame size       0x420 (1056 bytes)
frame 0          sequence 0x00000000, flash offset 0x20000
frame 262        sequence 0x80000106, flash offset 0x5ef10
trailing bytes   0

长晶 has no .rec container. Its staged rescue payload is the raw RW slice selected from the OpenTitan manifest.

On 长晶 the same inspection prints a different shape:

format:            epic-bin
proto:             A2.PI01
manifest offsets:  65536, 589824
manifest count:    2
rw length:         344076
version:           0.0.10
timestamp:         0
hash:              longjing_v0.0.ab14915686-aed7f95-l
security_version:  0

No header offset. The first 长晶 manifest is a binary struct nailed to a fixed position, not a key-value section you can grep. security_version lives in the manifest instead of an antirollback counter. 长晶 doesn't do framed rescue payloads, so theres no rec frame count to report. The timestamp is 0. 长晶 runs in the Pixel 11, and this eval image predates the August 12 launch, so the firmware carries no build date. The launch was still ahead of it. The RW payload is 344076 bytes, a third larger than the d3m2's 258048.

I said I wasn't finishing that joke. The manifest finished it for me. In bytes. In public.

The first manifest offset is 65536, exactly half the dauntless position. Suspiciously clean. 长晶's manifest carries an ECDSA-P256 signature, 64 bytes followed by 320 bytes of 0xa5 padding. Dauntless has no such field. Dauntless lays out its headers at 0x20000, 长晶 parks its first manifest at 0x10000. Two chips, two layouts, one wall between them and you.

Both 长晶 funny binaries carry valid manifest copies at 0x10000 and 0x90000, and their OTB0 identifiers sit at 0x10334 and 0x90334. The parser accepts those copies. It doesn't accept every accidental OTB0 byte sequence that happens to show up in the file, because false positives aren't firmware.

the 长晶 manifest

The structure at 0x10000 is an OpenTitan BL0 manifest_t, 1024 bytes long. It isn't a rescue-specific header. The boot manifest is doing a second job.

+0x000  signature                 0x180 bytes
+0x180  selector_bits
+0x184  device_id                 0x20 bytes
+0x1a4  manuf_state_creator
+0x1a8  manuf_state_owner
+0x1ac  life_cycle_state
+0x1b0  public_key                0x180 bytes
+0x330  address_translation
+0x334  identifier                OTB0
+0x338  manifest_version
+0x33c  signed_region_end
+0x340  length
+0x344  version_major
+0x348  version_minor
+0x34c  security_version
+0x350  timestamp                 8 bytes
+0x358  binding_value             0x20 bytes
+0x378  max_key_version
+0x37c  code_start
+0x380  code_end
+0x384  entry_point
+0x388  extensions                0x78 bytes
+0x400  BL0 code

To pick a rescue image, we need identifier, length, version_major, version_minor, security_version, and timestamp. Signature and public-key verification remain boot-ROM work. Thats a problem for a different idiot, on a different day.

funny binary  manifest bases       OTB0 offsets          RW length  version  security version  timestamp
pi01.ec.bin   0x10000, 0x90000     0x10334, 0x90334      0x5400c    0.0.10   0                 0
gb00.ec.bin   0x10000, 0x90000     0x10334, 0x90334      0x5212c    0.0.10   0                 0

The gb00.ec.bin from an eval board is the same story with a smaller RW payload, 336172 bytes, and proto A2.GB01. Same hash, same version, same security version, same timestamp, the same two manifest positions as pi01. Thats shared parsed metadata, not shipping history.

The header offset tells the parser where to start reading. The version tells the policy layer whether this image is too old for the device to accept. The infomap strikes drive the forward gate. The frame count isn't magic, its ceil(payload / 984). And every single frame carries its own SHA-256, which the rescue flow checks, because nothing in this protocol is taken on faith. Every single fucking frame.

Tests cover oem command output parsing, dauntless and 长晶 image inspection, .rec encode and decode, sparse frame decoding, rescue planning, multi-image retry, post-flash verification, post-rescue state checks.

The validation sequence doesn't change because the failure modes don't change. The chip finds new ways to die. Same checks. The meta never moves.

cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --check
perlica inspect-image fixtures/d3m2.ec.bin
perlica inspect-image fixtures/d3m2.ec.bin.rec
perlica inspect-image fixtures/epic-fixtures/pi01.ec.bin
perlica inspect-image fixtures/epic-fixtures/gb00.ec.bin

Hardware verification is a different boss. These checks make the parser give the same answers on the same funny binaries, every time, so the next bug is actually the bug and not just my machine having a moment.

Usually you're looking at a bad flash or a mismatched target family. Perlica checks partition state and target family before considering chip-internal failures. Nine times out of ten it isn't the chip. Touch grass before you blame the silicon. 草。

Illustration of two characters charging through a bright sky, one swinging a chainsaw, the other with a raised fist

what stays inside the chip

The chip narrows teh differential by refusing to answer. It just goes quiet and lets you cook. When both RW partitions lack ok and *, Perlica narrows the result to what it can observe from outside the chip. Partition state is usually the answer.

Perlica checks compatibility and policy before staging. For Dauntless, the observable GSC acceptance condition is RW_A selected and valid with a matching expected hash. It checks local images, framing structure, policy fields, oem command output, and post-rescue state. Everything it can see from outside the wall.

the execution boundary

The --yes flag separates the plan from the write. Every command before it is inspect, inform, suggest. The --yes command is the one that writes. This is the button. Once you press it, the run is live. Perlica follows the plan and checks the result.

the state machine

We check the state three times in the proposed firmware rescue flow. Inspect. Flash. Inspect. Stage. Rescue. Inspect again. Each gate answers a different question. Gate one wants to know what is broken. Gate two wants to know if the flash even produced a usable RW or just pretended to. Gate three wants to know if rescue left the partition matching the staged image, or if the chip called gg early. Writing the bytes only gets you to the next check.

a modelled Dauntless rescue-only validation

This trace comes from the source and unit tests. It isn't a record of recovery on hardware. This is a TAS. Tool-assisted. Every input recorded, no hardware on the line.

INPUT STATE
Chip: Dauntless (d3m2)
RW_A: * 0.2.2/rw_hash ok
/spil_

PLAN GATE
/spil_ present -> select a compatible intermediate rescue image
no compatible intermediate -> STOP: intermediate image required

PRE-STAGE GATE
rescue input needs a parseable version and hash string
missing either -> STOP: rescue image has no parseable version or hash

EXECUTE
stage -> fastboot oem gsc rescue -> re-inspect

POST-RESCUE GATE
RW_A must be selected and reported valid
expected hash, when present, must match the re-inspected RW_A hash
otherwise -> STOP: RW_A invalid or hash mismatch

The flash-then-rescue branch has its own first validation gate. After fastboot flash gsc <full image>, both RW slots must report valid and the selected full-image version tuple must match the supplied image. The tuple is generation, major, minor, and timestamp. Any invalid RW or timestamp mismatch stops before staging rescue.

A dry run prints the proposed plan without issuing a flash, stage, or rescue command. Scouting run. The chip sees nothing:

perlica --serial <SERIAL> rescue --rescue-image <PATH> --intermediate <PATH> --dry-run

Perlica keeps that order because it describes the device, not the operator. The chip doesn't know or care whether the hand holding it wears a badge. It checks the frame hashes either way.

Meme: a purple-haired character slouched in a gaming chair, captioned

larping as quarkslab

I don't explain GSC rescue flow. I don't make it easier to understand. I add to the mystery of the bricked phone by continuously spreading misinformation about it. And sometimes I make the screen do this: NOS production: error!

Quarkslab did this first. 2021. ARM Titan M, the chip everyone calls Citadel. They put out a toolkit and we have the actual code: bin2rec, a Ghidra loader, nugget_toolkit. They got a CVE. CVE-2022-20233, $75,000, Black Hat USA 22. One byte out of bounds in the Keymaster task, tripped on an ImportKey, turned into a ROP chain under W^X with a hardcoded canary, stack pivoted, read anywhere, decrypted key blobs walked out through forged NOS responses. They pwned the Titan M, for real money, in 2022. The 2021 Black Hat EU run was the recon: the layout, the map.

I mostly use IDA. Trying their Ghidra loader was part of the cosplay. It wants the 2021 Cortex-M image. We have an RV32 binary. It refuses ours. Even the larp has compatibility requirements.

I'm sick of the ISA wall. I try bin2rec. It cant be bothered. I try nugget_toolkit. It ports, with work. I try to just flash the thing. Titan M2 is RISC-V now, not ARM, and Quarkslab's gear only speaks ARM. Same nugget OS. Same NOS. New core. I only have the .rec, the manifest, and the oem gsc.

The .rec frame carries over, and basically nothing else. Quarkslab nailed the frame and then went home: 1056 bytes, two SHA-256s, the 0x00 to 0x01 ack rewrite, byte-for-byte the same on the M2. They got the frame right. The bin2rec packer decided that was enough. Fixed address, no header hunt, no magic check, and the decoder has a literal TODO verify hashes, and then it doesn't verify the hashes, which, frankly, is a fucking choice.

Our make-rec doesn't port bin2rec. It does the real flow: locate the RW header, check the 0xfffffffd magic, verify both hashes on the way back. bin2rec cracked the frame. We did the rest. It can keep the TODO.

It took a new packer to defeat a comment. 이겼다. 아무튼 이겼다.

I've portrayed us as the based ones and their rescue packer as the cringe one. We were supposed to borrow their tools. We have started making our own.

And if the packer wasn't enough, theres absolutely no full Dauntless running in an emulator on my workstation, acking every single frame and handing back a byte-perfect RW. There is no emulator.

Coincidence. All 263 of them. Im not taking questions.

We wanted the phone to boot. The workstation has volunteered instead. Wrong device. 順調です。

We aren't alone. Willem Hengeveld, nlitsme, 39C3, December 2025, the only public deep-RE of the Titan M2 firmware. His abstract complains Google added non-standard instructions to the RISC-V ISA. They did, sort of. The non-standard instructions are bit-manip. The actual crypto isn't an instruction at all. It is an MMIO peripheral, a custom bignum block with an elliptic-curve sidecar, sitting on the bus. Not OTBN. OTBN isn't even on this chip and I've already given it the better role. Dauntless doesn't run OTBN. The crypto is on the other end of a load and a store.

Perlica, our sussy wifey. We wanted a phone rescue. We gave her bin2rec's frame lineage, dragged it forward to a chip that swapped its core, and rebuilt the generation logic because Quarkslab's packer couldnt be bothered. Now the relationship has a version gate. She grabs me by the throat. I hand her a rescue image, she asks for the partition state. I hand her the frame hashes, she asks for the ack progression. I hand her all 263 acks, she isnt satisfied. I type --yes, spelled out, and she still runs one more inspection before saying anything nice to me.

Shes the rescue reimplementation, not the OS trust user build promised to us 3000 years ago. This article covers that rescue flow. We read the version gate, we frame the rescue, we check the partition came back. Affection isn't a verification result.

And then they announced the chip. Made by Google, August 12, 2026, ninety minutes of spectacle in New York, Trevor Noah up there doing his best against a line of Google execs visibly reading off a PR script, a parade of celebrities, Stephen Curry on a video, PinkPantheress closing it out with a live song that wasn't, tragically, Like a G6. Tensor G6. Titan M3. Codename Google Epic. Firmware 长晶. A glowing RGB camera bar they call Pixel Glow, because nothing says serious hardware security like a phone that cosplays as a gaming PC. Base model, eight hundred and ninety nine dollars. The whole press called it a cringe fest.

And through every celebrity, every glow, every dollar, GSC rescue protocol stayed on the wrong side of the wall. The chip is real now.

The wall didn't move.

This is an old rescue process, reconstructed from outside. Somehow it became a Sweet Dreams and the Holy Grail crossover that shipped without voice acting and a story nobody asked for. The chip fakes its own reboots and prints Secure boot: PRODUCTION while its bricked.

Planarcadia. So is this.

what it does not pretend to know

Live Dauntless and 长晶 rescue paths need broader hardware verification. For Dauntless, GSC accepts a rescued image when RW_A is selected and valid and its expected hash matches. Perlica validates compatibility and policy checks before reaching that state. She checks them twice. She checks me once, and doesnt like what she finds.

Perlica reimplements the reference rescue flow, not a fresh protocol invented from packet dumps. She follows that flow to handle state, pick images, and retry. The build hasn't yet recovered an external NOS device. Zero devices. Also zero hours of sleep, but nobody asked. Until that number moves, the last mile stays a test item, not a victory lap. No gg until the device actually boots. I am not typing gg. I am typing oem gsc into the dark like it owes me money.

Perlica gets another check every time we find another way this can fail. We find one roughly every forty minutes. She is getting bigger. She is getting stronger. Aura farming, but for parsers.

Perlica stays narrow. The plan goes on the screen before anything writes, and live execution stays off until you type the word yourself. Not -y. The whole word. Success is the device state matching what you staged, and not one byte sooner. Not one byte. I counted. I counted all of them.

For a firmware recovery attempt, checking what came back is the feature. Trust is a bug we haven't reproduced yet.

Rescue was never meant to be user-facing. The vendor kept it inside the wall. Perlica shows the person holding the brick which steps the host can see. Every gate, every hash, every refusal, visible. You deserve to watch it say no in high definition.

If the device boots and the bootloader says nothing, thats the success condition. The cleanest W in the stack. No log, no ack, no Perlica asking one more question. Silence. Ive never heard it. I think about it constantly.

Foggy city skyline under a spired fortress, with a typed line in Russian:

its 4am. the bottle is empty. the chip is still lying. im going to bed. they told me to touch grass. I touched silicon instead.

And if you somehow like this article and want to check out Perlica, please consider supporting us on our Patreon. I worked a gigantic amount of time on this one project and frankly its causing me to go insane. The wall is still up and so is the chip.

As always, more insane not gacha games and google confidential and proprietary review to come. And I hope that all of you are having a very nice night tonight or something. Honestly, after forty-three revisions of this thing, I have forgotten how to read my own draft.