8000 firehose: add OPPO/OnePlus VIP (verified image protection) support by 2loch-ness6 · Pull Request #829 · bkerler/edl · GitHub
[go: up one dir, main page]

Skip to content

firehose: add OPPO/OnePlus VIP (verified image protection) support - #829

Open
2loch-ness6 wants to merge 1 commit into
bkerler:masterfrom
2loch-ness6:master
Open

2loch-ness6 wants to merge 1 commit into
bkerler:masterfrom
2loch-ness6:master

Conversation

@2loch-ness6
Copy link
Copy Markdown

Adds support for OPPO/OnePlus "VIP" (verified image protection): the digest-table handshake
that newer OPPO/OnePlus/Realme programmers (SM8650 and up) demand before they accept any
read, write or erase.

Why

I wrote this because I had no other way out. My OnePlus 12 (CPH2581) was left unusable
after a bad OxygenOS 15.0.0.863 update, fastboot was gone too, and every thread I found
said the only remaining option was a paid unlock/flash tool - SOL otherwise. The public
loaders simply refused to talk to the device:

firehose - INFO: VIP is enabled, receiving the partition info of size 36864
firehose - [LIB]: ERROR: label cannot be null

That is the same wall half of this issue tracker has been hitting for years (#439, #481,
#492, #605, #698, #730, #751, ...). This patch is what got the phone back: full EDL
access again, GPTs on every LUN, partition reads, and the writes that restored it. It
seemed a waste to keep that to myself, so here it is, cleaned up, for everyone who was
told they needed a paid tool for the same fix.

What the target expects

The programmer wants, in this order:

  1. the signed digest table (Digest.elf / Digest.mbn) as a raw, non-XML packet
  2. <?xml version="1.0" ?><data><verify value="ping" EnableVip="1"/></data>
  3. the signature (Sign.bin, 256 bytes padded to 4096) as a raw packet

After that, every read/write/erase has to carry label="<partition>" naming a partition
of the digest table, otherwise the target answers ERROR: label cannot be null.

Implementation

  • --vip, --vip-digest=file, --vip-sign=file, --vip-label=label plus
    --skipinitialread. Since they apply to every command, they are pulled out of
    argv before docopt and merged into the parsed arguments, rather than being repeated
    in every one of the ~46 usage lines.
  • firehose.connect() detects the announcement (receiving the partition info /
    receiving the signed table / receiving the sign table) and runs the handshake. The
    target's handshake log is fed through the normal log parser, so supported_functions
    still works afterwards. On failure, vip_ok is set to False and configure() refuses
    to continue instead of producing misleading timeouts.
  • firehose.parse_vip_table() parses the SVIP digest table (magic 0x7CEF0312) into a
    per-partition map of permissions, start sector and size. Informational only; the target
    is the one that enforces access. Entries are per LUN but names repeat across LUNs, so the
    map is keyed by name. Parsing is best effort: a bare .mbn without the SVIP header just
    warns and the handshake still runs.
  • cmd_read(), cmd_read_buffer(), cmd_program(), cmd_program_buffer(),
    cmd_erase()
    emit the label= attribute. printgpt uses PrimaryGPT; r, rl,
    w, wl and e derive it from the partition they are handed; raw sector access
    (rs, rf, ws, wf, es) uses --vip-label because there is no partition name to
    fall back on. The attribute is only emitted on VIP targets, so the XML sent to every
    other device is unchanged.

Also fixed on the way, because they got in the way of the handshake:

  • cmd_read_buffer() raised KeyError: 2 when the target replied without the expected
    response fields, which hid the real error text (e.g. the permission error below).
  • connect() only parsed the first line of each USB read as a log line; the rest was
    dropped.
  • cmd_read_buffer() retried forever when the target stopped sending data mid read.

Usage

edl printgpt --memory=ufs --lun=0 --vip --vip-digest=Digest.elf --vip-sign=Sign.bin --skipinitialread
edl r  abl_b abl_b.img --memory=ufs --lun=4 --vip --vip-digest=Digest.elf --vip-sign=Sign.bin --skipinitialread
edl w  abl_b abl_b.img --memory=ufs --lun=4 --vip --vip-digest=Digest.elf --vip-sign=Sign.bin --skipinitialread
edl rl dumps          --memory=ufs       --vip --vip-digest=Digest.elf --vip-sign=Sign.bin --skipinitialread
edl rs 517223 256 abl_b.img --memory=ufs --lun=4 --vip --vip-digest=Digest.elf --vip-sign=Sign.bin --vip-label=abl_b

--vip-sign is optional: without it the handshake stops after the digest table, which is
all that some programmers ask for. configure() still has to run, it is what initialises
the storage; --skipinitialread only skips its doomed probe read. The README section
documents all of this.

Testing

Done on a OnePlus 12 (CPH2581, SM8650, UFS 512 GB, CPH2581_15.0.0.863(EX01)), which is
where the tool was needed in the first place:

  • handshake against the 187-entry digest table that ships with the firmware, digest table
    parsed into 173 unique partition names
  • printgpt on all LUNs, including the LUN 0 super extent and the PrimaryGPT /
    BackupGPT entries that repeat per LUN group
  • partition and raw sector reads (~8 MB/s), reads of digest-pinned partitions (read-only
    ones included)
  • partition writes (~38 MB/s) to unlocked partitions, read back and verified by hash
  • the XML produced for non-VIP targets and for these validated VIP paths was compared byte
    for byte against the code as it behaved on hardware, to make sure nothing else changes

CI: flake8 . --count --select=E9,F63,F7,F82 is clean and the advisory run reports 140
items vs. 141 before the patch, i.e. no new warnings.

Related issues

Related OPPO/oplus reports that are not addressed here: #794 and #799 (OPD2415/SM8750
fail in Sahara with AUTH_FAILURE before firehose even starts, so they are a different
stage of the boot chain) and the older OPPO devices in #207/#390/#458, which predate VIP.

Known limits

  • Only the digest-table VIP variant is implemented; the command-hash variant is not.
  • cmd_erase() carries labels for completeness, but erasing was not exercised on VIP
    hardware (reads and writes were).
  • On my device the digest-entry permission flags are enforced for reads (r:0 gives
    ERROR: not get permission,line=141 / not allowed on external network), while the
    SHA256 footer check of a write to a pinned partition appeared to be advisory - a write to
    a digest-pinned partition landed and only reported a sha256final mismatch. That was
    tested on a single partition, so I would not rely on it; treat the digest table as
    enforced.

Newer OPPO/OnePlus/Realme targets (SM8650/OxygenOS 15 and up) enable
OPPO's VIP extension: firehose refuses every storage command until the
host has handed it the signed digest table and signature. With stock
edl.py such a target just fails, e.g. "Failed to open the UFS Device" or
"label cannot be null".

Implement the host side of the handshake:

- --vip, --vip-digest, --vip-sign, --vip-label: the digest table
  (Digest.elf / Digest.mbn) and signature (Sign.bin) files used by the
  official OPLUS tool. Because they apply to every command, they are
  stripped from argv before docopt sees them instead of being added to
  each usage line.
- firehose.connect(): when the target announces VIP with its "receiving
  the partition info of size N" prompt, send the digest table, then
  <verify value="ping" EnableVip="1"/>, then the signature. If any step is
  rejected, mark VIP as failed and refuse to configure the target.
- firehose.parse_vip_table(): parse the SVIP digest table (magic
  0x7CEF0312) into a per-partition map of permissions, start sector and
  size.
- cmd_read(), cmd_read_buffer(), cmd_program(), cmd_program_buffer(),
  cmd_erase(): emit the label= attribute that VIP targets require in order
  to authorise access. It defaults to --vip-label ("PrimaryGPT" for GPT
  reads) and is only emitted on VIP targets, so the XML sent to every
  other device is unchanged.
- --skipinitialread: skip configure()'s initial storage-sector probe.
  configure() itself still has to run, it is what initialises the storage.
- cmd_read_buffer(): use the parsed response dict in the error path
  instead of the legacy tuple indexes. Indexing rsp[2] raised KeyError and
  hid the real target error message. Also stop retrying forever when the
  target stops sending data mid read.
- firehose.connect(): parse every log line of a USB read instead of only
  the first, the remaining lines were dropped.

Verified on a OnePlus 12 (CPH2581, SM8650, UFS, 187 signed digest
entries): target authentication, GPT parsing on all LUNs, reads of
locked and unlocked partitions, and writes to unlocked partitions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0