Questo è il diario di un progetto che doveva essere una serata e si è trasformato in una piccola masterclass di EDL, firehose e reverse engineering di bootloader. Obiettivo semplice: far girare Kali NetHunter su un OnePlus 7 GM1900. La realtà: il telefono si rifiuta di sbloccare il bootloader con un laconico “Device cannot be unlocked for technical reason”, e per capire il perché ho dovuto scendere fino a scrivere partizioni grezze via Qualcomm EDL.

Lo scrivo come l’ho vissuto, dead-end compresi. Anzi, soprattutto quelli.

Il device (e il primo campanello)#

Il paziente: OnePlus 7 GM1900, codename guacamoleb, Snapdragon 855 (SM8150), storage UFS, 256 GB, slot A/B. Letto tutto via adb/fastboot senza sudo.

Modello:      GM1900 (guacamoleb)
Android:      12 / OxygenOS 12  (build GM1900_11_H.41)
Region mark:  US   (ro.oem.key1=US, regionmark=US)

Ed ecco il primo campanello: hardware GM1900 = variante cinese, ma software e region mark US/Global. È un OnePlus 7 cinese convertito a OxygenOS Global in qualche vita precedente. Tienilo a mente, perché è il filo rosso di tutta la storia.

Il muro: “technical reason”#

Sblocco OEM attivato, sys.oem_unlock_allowed=1, telefono già resettato. Lancio il comando di rito:

$ fastboot flashing unlock
FAILED (remote: 'Device cannot be unlocked for technical reason')
$ fastboot oem unlock
FAILED (remote: 'Device cannot be unlocked for technical reason')

Nessuna schermata di conferma, solo il muro. La prima ipotesi ovvia è il cooldown anti-furto di OnePlus dopo un reset: il token di sblocco viene provisionato server-side solo dopo un po’ di utilizzo. Aspetto giorni, tengo il telefono online. Niente.

Poi la diagnostica che smonta l’ipotesi:

$ fastboot flashing get_unlock_ability
get_unlock_ability: 1

1 = il bootloader autorizza lo sblocco. Eppure i comandi falliscono. Quindi non è policy, non è cooldown, è qualcosa a un livello più basso. Nota di servizio: i getvar su singola variabile sul OP7 falliscono a caso con “Variable Not found”, quindi uso sempre fastboot getvar all.

Provato e fallito, dalla parte “facile”#

Prima di tirare fuori l’artiglieria ho esaurito le vie a basso rischio:

  • Cavo e porta USB diversi: nessun cambiamento.
  • Downgrade a OOS11 via updater: impossibile, l’app serve solo l’ultima versione.
  • Re-flash della OOS Global via local upgrade (3.25 GB, ufficiale): eseguito, ma dopo il flash il device si identifica ancora come GM1900_11_H.41 e resta bloccato. Il local upgrade non tocca le partizioni di basso livello (persist, oem, opproduct) dove vivono identità e stato di lock.

Diagnosi intermedia: il mismatch di identità sta nelle partizioni basse. Per riscriverle serve EDL.

Scendere in EDL#

EDL (Emergency Download Mode, Qualcomm 9008) parla il protocollo Sahara + Firehose. Setup su Kali, interamente da Linux, niente MSM Tool Windows:

edl-venv/   # edlclient 3.62 (bkerler), installato da sorgente su Python 3.14
Loaders/    # 887 firehose loader
oppo_decrypt/  # opscrypto.py + ofp_qc_decrypt.py

Regola udev per 05c6:9008, gruppo plugdev, e (dettaglio che fa perdere ore) va fermato ModemManager, che altrimenti si mangia il device appena appare:

$ sudo systemctl stop ModemManager

Poi il quirk numero uno del OnePlus 7: non resta in EDL abbastanza a lungo per completare l’handshake Sahara. La mossa vincente è coordinata:

Avvii edl PRIMA (resta in “Waiting for the device”), POI mandi adb reboot edl. Così edl aggancia il telefono nell’istante esatto in cui appare e parte subito.

Il loader#

Gap teorico: nel repo Loaders non c’è il firehose del OnePlus 7 liscio. Provo per disperazione quello del 7T, e funziona:

HWID:     0x000a50e100514985
MSM_ID:   0x000a50e1
CPU:      SM8150
PK_HASH:  2acf3a85fde334e2...

Il 7 e il 7T condividono msm_id e pk_hash, quindi il PBL accetta il loader del 7T. (Provare un loader è a rischio zero: se l’ID non combacia, il PBL lo rifiuta e basta.) Firehose connesso, UFS Samsung, GPT completa dumpata su 6 LUN. Backup read-only di devinfo, config, param, frp, keystore, secdata, dip. Restore point in tasca prima di scrivere un solo byte.

Tentativo 1: flippare il bit in devinfo#

La partizione devinfo (Lun4, magic ANDROID-BOOT!) ha uno stato leggibile:

offset 0x0D  is_unlocked = 0x00   (locked)
offset 0x0E  is_tampered = 0x00
offset 0x0F  is_verified = 0x01

Teoria: se metto is_unlocked = 0x01 e riscrivo, l’ABL all’avvio si crede sbloccato senza passare dal comando fastboot. Preparo il file con un solo byte di diff, verifico, scrivo:

$ edl w devinfo dumps/devinfo-unlocked.bin
Wrote to sector 962718

Reboot. Ignorato. L’ABL resetta il flag: flash.locked=1, verifiedbootstate=green, unlocked false. Il OnePlus 7 cross-verifica devinfo contro il blob config (un HMAC firmato per-device, opaco, non falsificabile). devinfo non è il lever. Nessun danno, ma vicolo cieco.

Tentativo 2: restore MSM completo (e un bug in edl)#

Se il problema è il mismatch di identità, la cura è un restore di fabbrica tipo MSM. Il pacchetto OnePlus è un .ops cifrato: lo estraggo con opscrypto.py in rawprogram*.xml, patch*.xml, immagini e firehose, poi flasho con edl qfil.

Primo bersaglio: OxygenOS 10.3.8 Global (GM57AA). Flash che dichiara successo, ma il device è ancora OOS12 e tutti i write sono a 0.00 MB/s, “finito subito”. Qualcosa non scrive davvero.

Vado a leggere il sorgente di edl e trovo il bug: il ramo qfil in firehose_client.py chiama cmd_program senza prima chiamare writeprepare(), che invece i rami w/wl/wf chiamano. writeprepare() manda l’auth OnePlus (demacia + setprojmodel), che sblocca la protezione di scrittura. Senza, le scritture sulle partizioni protette vengono rifiutate in silenzio. (Ecco perché edl w devinfo aveva scritto: passava da writeprepare.)

Patch di una riga:

# firehose_client.py, ramo qfil, subito dopo "[qfil] raw programming..."
self.firehose.modules.writeprepare()

Il messaggio “Oneplus protection with prjid 100439 detected” mi conferma il progetto: 100439 in decimale è 0x18857, cioè 18857, il nostro device esatto.

Riflasho la Global OOS10 con la patch: stavolta write veri. Ma al boot va in SMT / bootloop. La Global su hardware cinese va in crisi di region (provisioning persist/oem incoerente). Serve la ROM cinese nativa.

Tentativo 3: HydrogenOS cinese nativa#

La trovo su Halab Tech: GM1900domestic_11_H.40_0323, ProjectNo 18857, base Android 12. È un .ofp (non .ops), quindi si decripta con ofp_qc_decrypt.py, che sputa fuori direttamente rawprogram*.xml, patch*.xml, firehose e immagini. Dettaglio download: il link Halab rimbalza sul CDN OnePlus che dà 403 al click, e si scarica con wget aggiungendo il Referer:

$ wget --referer="https://support.halabtech.com/" "<cdn-url>"

Flash via edl qfil con la mossa coordinata e la patch writeprepare: write reali a 14-27 MB/s, patching della GPT eseguito, is now bootable. Reboot, logo OnePlus, e parte il setup di HydrogenOS in cinese. Il telefono è tornato nativo e boota. Vittoria? No.

$ fastboot flashing unlock
FAILED (remote: 'Device cannot be unlocked for technical reason')

Ancora. Su firmware cinese nativo, identità coerente, OEM unlock attivo, online. Il muro sopravvive a tre firmware completi. A questo punto la teoria del mismatch CN/Global è ufficialmente morta.

La svolta: non è hardware, è un bug di Android 12#

Incrocio il reverse engineering con la ricerca in community, e salta fuori la verità, documentata su community.e.foundation (thread guacamoleb) e reddit r/oneplus: “technical reason” sul OnePlus 7 è un bug noto dell’abl di Android 12. Il fix della community è secco:

Scendi ad Android 11, fastboot oem unlock funziona, e installi subito la ROM custom senza tornare ad Android 12.

Il motivo per cui sbattevo sempre la testa: sono sempre stato su Android 12. La GM1900 originale (H.41) è A12, la HydrogenOS H.40 è A12. Il bug è nell’abl A12. La Global OOS10 (A10, senza bug) l’avrebbe sbloccata, ma andava in SMT per la region. Ho esplorato tutte le combinazioni tranne quella giusta: cinese e Android 11.

Un po’ di reversing dell’abl#

Per curiosità sono andato dentro l’abl.elf: è un ELF firmato, dentro un volume UEFI FV, con un blob LZMA a 0x3078, decomprimibile con il modulo lzma di Python in formato FORMAT_ALONE (643272 byte). Nel decompresso, la logica di sblocco in chiaro:

IsAllowUnlock
Error Reading FRP partition
Unable set the unlock value
set_boot_info_to_rpmb / km_client_read_rpmb_boot_info

Lo stato di lock vive in RPMB (Replay Protected Memory Block), i dati di unlock su oplusreserve1, con verifica RSA del token. Tradotto: patchare l’abl non serve. Con secure boot locked il device rifiuta un abl non firmato, e per aggirarlo servirebbe glitching del SoC, fuori portata e fuori scopo. La strada pulita resta il downgrade ad Android 11.

La caccia al firmware (e una trappola che salva dal brick)#

Il target era chiaro: guacamoleb_14_H.01, HydrogenOS cinese Android 11 (branch H). Cinese, quindi boota nativo senza SMT. A11, quindi niente bug di sblocco. Il salto minimo da A12.

Trovarlo è stato metà del lavoro, e la metà più frustrante — perché non è un problema tecnico, è un problema di ecosistema. Il firmware cinese vecchio del OnePlus 7 non sta sul sito OnePlus, non sta su un mirror pulito: vive su portali di “firmware repair” per riparatori, dietro paywall, limiti giornalieri e link Google Drive che campano una settimana.

Il giro che ho fatto, nell’ordine:

  • Halab Tech: ce l’ha, ma dopo un paio di download va a limite giornaliero. Aspetti 24h, riprovi, e magari nel frattempo il file che ti serve è su un mirror Drive con la quota di download esaurita (“Superato il limite” anche a file intatto).
  • Account nuovi + VPN: cambio IP, nuovo account, e mi becco lo stesso “hai superato il limite”. Il rate-limit è lato file, non lato IP.
  • Chiedere l’accesso al Drive come viewer, scrivere su WhatsApp al mantainer con l’account con cui ho fatto la richiesta… tempi biblici, per un hobby serale.

Alla fine mollo €4.99 su romdevelopers per un pacchetto da 2 download. Non è tanto il costo — sono cinque euro — è il principio: nel 2026 devo pagare un riparatore terzo per un firmware che il produttore stesso distribuiva, e per giunta il pagamento non è immediato ma “fai la richiesta e poi paghi”, altra attesa. Ma a quel punto volevo solo il file.

E i due download si rivelano due link Drive. Errore mio istintivo: “saranno due mirror dello stesso file”. No. Erano due file diversi, e qui si rischiava il brick.

Il primo (5 GB, etichettato guacamoleb_14_H.01_211221) lo scarico, lo apro e — regola d’oro — prima di avvicinarmi a edl qfil verifico il build.prop:

$ grep -E "sdk|release|fingerprint" build.prop
ro.build.version.sdk=31
ro.build.version.release=12
ro.build.fingerprint=OnePlus7:12/SKQ1.211113.001/...

sdk=31, release=12. È Android 12. Il nome file mentiva: dentro c’era GM1900domestic_11_H.36, e quel 11 in domestic_11 non è la versione di Android, è il ramo di HydrogenOS. Anche H.36 è A12, col bug. Se mi fossi fidato del nome avrei riflashato lo stesso problema — o peggio.

Lezione da incorniciare: verifica sempre il build.prop, mai fidarti del nome del file. Il campo che conta è ro.build.version.sdk.

Il secondo link (4.2 GB, un vero .tar.bz2) è invece quello giusto. La conferma arriva dal file marker interno e dall’MD5 del pacchetto:

rev_China_211221_..._cve2021-12-01_ufs_Hydrogen-OS-11.0.5.1.GM57
guacamoleb_14_H.01_211221.ops   MD5 1f3214ce...  ✓ (== md5sum_pack.md5)

Hydrogen-OS 11, patch dicembre 2021, cinese. Finalmente un pre-A12 confermato, non solo sperato.

Prep del flash#

Il .ops si decripta con opscrypto.py in immagini + firehose:

$ opscrypto.py decrypt guacamoleb_14_H.01_211221.ops
→ extract-h01/  (prog_firehose_ddr.elf + 61 immagini)

Il settings.xml conferma il progetto (18857, UFS, 6 LUN). Da lì genero i rawprogram0-5.xml e patch0-5.xml, verifico che ogni immagine referenziata sia sul disco, e ricontrollo che la mia patch writeprepare sia nel ramo qfil di firehose_client.py. Loader nativo del guacamoleb stavolta, niente più prestito dal 7T.

La guerra con adb (che non mi aspettavo)#

Telefono collegato, USB debugging attivo, e adb devices… vuoto. Ore su un problema che sembrava banale. L’interfaccia ADB c’era nei descrittori USB, i permessi erano corretti (plugdev, ACL sul nodo, regola udev con uaccess), la chiave adbkey a posto. Eppure niente.

Il trace svela il colpo:

$ ADB_TRACE=all adb nodaemon server
[ usb opened /dev/bus/usb/003/059, fd=15]
[ usb ioctl(15, USBDEVFS_CLAIMINTERFACE) failed: Device or resource busy]

CLAIMINTERFACE: Device or resource busy. Qualcun altro tiene l’interfaccia. Il OnePlus 7 espone MTP + ADB insieme, e su Kali/Xfce c’è gvfs-mtp-volume-monitor che, appena vede il telefono, lo afferra via libusb — sysfs lo conferma, driver=usbfs su entrambe le interfacce, ADB compresa. adb perde sempre la corsa. E ucciderlo non basta: dbus lo fa risorgere a ogni ri-enumerazione del device.

Prima mossa: impedire il respawn con un override nel bus utente (niente sudo):

$ cat ~/.local/share/dbus-1/services/org.gtk.vfs.MTPVolumeMonitor.service
[D-BUS Service]
Name=org.gtk.vfs.MTPVolumeMonitor
Exec=/bin/true
$ dbus-send --session --type=method_call --dest=org.freedesktop.DBus \
    /org/freedesktop/DBus org.freedesktop.DBus.ReloadConfig

(Stesso trattamento per GPhoto2VolumeMonitor e AfcVolumeMonitor.) Ma dopo aver ammazzato gvfs, CLAIMINTERFACE era ancora busy — con nessun processo che teneva il nodo. Un claim orfano rimasto appiccicato nel kernel dopo un vecchio adb ucciso con -9. Si libera solo con un reset della porta USB, che si può fare da userspace via ioctl:

import os, fcntl
USBDEVFS_RESET = ord('U') << 8 | 20   # 0x5514
fd = os.open("/dev/bus/usb/003/066", os.O_WRONLY)
fcntl.ioctl(fd, USBDEVFS_RESET, 0)     # forza la ri-enumerazione pulita

Dopo il reset le interfacce tornano driver=none, riavvio adb e finalmente:

$ adb devices -l
d04764ee   device   product:OnePlus7_CH   model:GM1900   transport_id:1

device, non unauthorized. Aggancio pulito. (E no, non era conky: il suo hwlab.sh usa lsusb liscio, che legge i descrittori senza fare CLAIMINTERFACE.)

Dove sono ora#

Stato del telefono confermato via adb, ed è esattamente lo scenario da manuale del bug:

Android:            12 (sdk 31)
Bootloader:         locked  (verifiedbootstate green, flash.locked=1)
sys.oem_unlock_allowed: 1   (toggle OEM unlock ON)

A12 + locked = fastboot oem unlock sbatterebbe sul “technical reason”. Tutto il resto è armato: .ops A11 decrittato, rawprogram/patch generati, loader nativo, writeprepare verificato, adb che aggancia. Manca una cosa sola, la più stupida: la batteria è al 14%, e in EDL il telefono non carica. Flashare adesso, con il rischio che muoia mentre scrive xbl/abl, sarebbe l’unico modo di brickarlo per davvero. Quindi carica, e io aspetto.

La sequenza finale, appena la batteria è su:

edl qfil rawprogram0..5.xml patch0..5.xml . --loader=prog_firehose_ddr.elf
   ↑ lanciato PRIMA (Waiting for the device)
adb reboot edl        ← entrata EDL pulita, il device resta agganciato
reboot  →  boota Android 11 CN
fastboot oem unlock   ← senza il bug A12, stavolta deve passare
→ LineageOS 23.2 (dtbo/vbmeta/boot + sideload) + Magisk + modulo NetHunter

Il flash che non partiva: una regressione del kernel#

Batteria carica, tutto pronto, lancio edl qfil. E il telefono, appena entra in EDL, si resetta e sparisce:

main - Device detected :)
DeviceClass - USBError(5, 'Input/Output Error')
Device is in Sahara error state

Stesso identico errore a ogni tentativo. Cambio cavo: niente. Cambio metodo di ingresso EDL: niente. Rimuovo qcserial (il driver kernel che si mangiava il device 9008): serve ma non basta. Disabilito l’autosuspend USB: niente.

# qcserial afferra il 9008 appena appare: toglilo e blackliistalo
lsmod | grep qcserial
echo 'blacklist qcserial' | sudo tee /etc/modprobe.d/edl-qcserial.conf
sudo modprobe -r qcserial

# autosuspend USB (a volte fa cadere il device durante il Sahara)
cat /sys/module/usbcore/parameters/autosuspend      # valore attuale
echo -1 | sudo tee /sys/module/usbcore/parameters/autosuspend   # -1 = off

Poi guardo la data. Il flash che aveva funzionato era di tre giorni prima. Nel mezzo, il sistema aveva fatto un reboot in un kernel nuovo (7.0.12, era 6.19.14). L’unica variabile davvero cambiata. Riavvio nel kernel vecchio — X non parte nemmeno, ma dal terminale nudo il flash è tutto ciò che serve — e:

$ uname -r
6.19.14+kali-amd64
$ edl qfil ...
Write (Sector 0x100 of 0x2000) 16.09 MB/s

Come tornare a un kernel precedente senza rincorrere il menu GRUB al boot:

uname -r                          # kernel attuale
ls /boot/vmlinuz-*                # kernel installati
# imposta SOLO il prossimo avvio sul kernel vecchio, poi riavvia:
sudo grub-reboot "Advanced options for Kali GNU/Linux>Kali GNU/Linux, with Linux 6.19.14+kali-amd64"
sudo reboot

Parte. Era una regressione xHCI del kernel 7.0.12: il PBL di Qualcomm è troppo sensibile al timing, e qualcosa nel nuovo driver USB gli faceva cadere il device durante l’handshake Sahara. Lezione: quando tutto è identico tranne una cosa, è quella cosa — anche se “quella cosa” è il kernel sotto i piedi.

Un secondo bug, nel file sbagliato#

Il flash però falliva ancora, stavolta sull’auth OnePlus: setprojmodel ... failed, ERROR: SN. edl legge il project id dalla partizione param e su questo device lo leggeva sballato (104453 invece di 18857). Patcho oneplus.py per forzare il progetto giusto… e non cambia niente. Perché? Perché stavo patchando il file sbagliato: l’eseguibile caricava la copia in site-packages, non quella del repo git che stavo editando. Un’ora buttata su un sys.path.

La riga che ti evita quell’ora — chiedi a Python quale file sta davvero importando:

# stampa il path REALE del modulo che verrà usato a runtime
python3 -c "import edlclient.Library.Modules.oneplus as m; print(m.__file__)"
# poi cancella la cache dei .pyc dopo aver editato, o modifiche ignorate:
find . -name '__pycache__' -type d -exec rm -rf {} +

Patchata la copia giusta:

[qfil] running OnePlus writeprepare/auth...
Oneplus protection with prjid 100439 detected      ← 0x18857, corretto
Write (Sector ...) 18 MB/s
...
[qfil] patching ok
Setbootablestoragedrive succeeded
[qfil] partition(1) is now bootable

56 partizioni su 56. Android 11 flashato. Reboot. Logo OnePlus. E poi…

SMT download failed#

Schermo rosso:

<Error!>
SMT download failed! Can only be fixed by SMT download
This is caused by download interruption.

Il primo tentativo di flash, giorni prima, era stato troncato da un timeout che avevo messo io per prudenza — e quell’interruzione aveva settato un flag “download di fabbrica interrotto” che vive nel profondo del device. Ironia: la prudenza che doveva salvarmi ha creato il muro successivo.

Lo leggo via edl e vado a guardare il byte con le mani:

# dump della partizione param dal telefono in EDL
edl r param param_live.bin --loader=<firehose_loader>
# SMT_Download_Status vive a SID 0xC (DOWNLOAD), offset 0x30
#   → offset assoluto = 0xC*0x400 + 0x30 = 0x3030 (1 byte)
xxd -s 0x3030 -l 1 param_live.bin        # → 00000030: 00   ...pulito!
xxd -s 0x18   -l 5 param_live.bin        # bonus: il projid ("18857")

Il flag SMT_Download_Status è a 0x00 — cioè “pulito”. Quindi non è quel byte: lo stato SMT vive altrove (RPMB, o lo chiude solo l’handshake proprietario di fine-download dell’MSM ufficiale, che edl non invia). Tradotto: edl scrive tutte le partizioni ma non dice mai al device “il download di fabbrica è finito con successo”. Serve l’MSM.

MSM, la macchina virtuale, e perché il PBL odia le VM#

L’MSM è Windows-only, closed-source. Provo con una VM VirtualBox. E qui una lezione di sistema che val la pena scriversi: il passthrough USB di una VM non regge il Sahara. L’MSM arriva a parlare col device — enumerazione ok, primi pacchetti Sahara ok — poi crolla:

Sahara Communication Failed, Please try again (PBL:6)

Il PBL è un caricatore minimale con timeout stretti, pensato per una connessione diretta di fabbrica. VirtualBox intercetta l’USB a livello di URB e fa da proxy host→guest: latenza e jitter che l’upload bulk del loader firehose non tollera. Enumerazione (control transfer, piccoli, robusti) → passa. Upload del loader (bulk, sostenuto, timing-critico) → il PBL scade. Su USB nativa non succede. È documentato: per l’MSM serve bare-metal Windows, punto.

Il campo di battaglia Windows#

Recuperato un vero PC Windows (uno che controllo io — mai flashare telefoni sulla macchina aziendale: policy, security, tracce), comincia una seconda guerra, tutta fatta di dettagli idioti che ti fanno perdere ore. La metto per esteso, perché è esattamente quello che cercherai alle 3 di notte.

Cavi, cavetti e porte#

Prima ancora dell’MSM, l’USB. Il device in EDL enumera come Qualcomm HS-USB QDLoader 9008 — o dovrebbe. Invece:

  • La dock non passa il fastboot. Attraverso l’hub della docking station il device fastboot (18d1:d00d) non si presentava proprio. Diretto al PC: appare subito. Regola: per fastboot/EDL, cavo dritto nella porta del PC, mai attraverso hub o dock.

  • Cavi che enumerano ma non trasmettono. Il sintomo, sul kernel Linux di fianco: device descriptor read/64, error -71 a ripetizione, unable to enumerate USB device. error -71 (EPROTO) è la firma di un cavo dati morto: carica, fa vibrare, sembra vivo, ma sui trasferimenti dati ad alta velocità crolla. Ne ho bruciati tre prima di trovare quello buono. Se vedi -71, non è il telefono, è il cavo — cambialo prima di impazzire.

    Come lo leggi tu stesso — guarda il kernel in tempo reale mentre colleghi il cavo:

    # log del kernel live: collega/scollega e osserva
    sudo dmesg -w | grep -iE "usb [0-9]-|error -71|not accepting|enumerate"
    # cavo BUONO  → "New USB device found, idVendor=05c6, idProduct=9008"
    # cavo MORTO  → "device descriptor read/64, error -71 ... unable to enumerate"
    
    # e verifica che il device ci sia davvero:
    lsusb | grep -iE "05c6:9008|18d1:d00d|22d9"   # EDL / fastboot / MTP-adb
    

Il driver non firmato e il ballo dei riavvii#

Windows 11 vede il device ma con il triangolo giallo, Codice 52: “impossibile verificare la firma digitale del driver”. Il driver QDLoader che gira in giro non è firmato, e Windows 11 by default rifiuta i driver non firmati.

Per installarlo devi disabilitare l’imposizione firma driver. Due modi:

  • Una tantum (Impostazioni → Sistema → Ripristino → Avvio avanzato → Riavvia → Risoluzione problemi → Opzioni avanzate → Impostazioni di avvio → Riavvia → premi 7). Vale solo per quel boot: al riavvio dopo, la firma è di nuovo attiva e il device torna giallo. Non riavviare finché non hai finito.

  • Persistente, da cmd amministratore, se prevedi più riavvii:

    bcdedit /set testsigning on
    

    (compare un watermark “Modalità test”, innocuo, si toglie con testsigning off).

E qui la trappola successiva: se il disco è cifrato con BitLocker, cambiare le impostazioni di boot/firma fa scattare la richiesta della chiave di ripristino a ogni riavvio. La soluzione è sospendere BitLocker per un tot di riavvii — così riavvii in pace e si riattiva da solo dopo:

Suspend-BitLocker -MountPoint "C:" -RebootCount 5     # PowerShell admin
manage-bde -protectors -disable C: -RebootCount 5     # oppure cmd admin

La COM introvabile#

Driver installato, firma disabilitata, triangolo sparito… e l’MSM che resta su “waiting for device”. Perché? Guardo in Gestione dispositivi: la porta QDLoader era finita a COM280. L’MSM scansiona solo le COM basse (grossomodo 1–20) e non arriva lì. Fix: proprietà della porta → Impostazioni porta → Avanzate → Numero porta COM → un numero basso e libero (occhio a quelli marcati (in use) da porte fantasma di device vecchi — si liberano con Mostra dispositivi nascosti + disinstalla). Poi in MSM premi Enum per ri-scansionare.

Bonus: se il device compare sotto “USB” invece che sotto “Porte (COM e LPT)”, Windows gli ha legato il driver sbagliato (magari WinUSB da Zadig) — va rilegato al driver seriale QDLoader, altrimenti nessuna COM e l’MSM è cieco.

L’MSM giusto, la password, la data#

Ultimo strato di cipolla. L’errore SMT si chiude solo con la SMT Download mode dell’MSM — che è una modalità di fabbrica, di default bloccata. E qui:

  • La versione conta. La V5.1.70 (la più recente) in SMT mode crasha. La V5.0.15 no. Cercare la build giusta, non “l’ultima”.
  • Serve la password di fabbrica. Per l’MSM OnePlus è te123, e la memoria va scelta a mano (il mio è il 256 GB → 256G; sbagliarla partiziona per la dimensione errata).
  • Il trucco della data. Anche con la password giusta, la SMT mode crashava. Nei video dei riparatori il dettaglio: disabilitare internet e spostare la data indietro, perché il tool ha un check su un certificato scaduto. La data va messa dentro la validità del certificato — per la mia firmware di fine 2021, non nel 2017 come nei tutorial di roba più vecchia.
  • Dettagli che fanno crashare a prescindere: l’MSM va lanciato come amministratore, da una cartella locale (non da chiavetta/rete, va in crash), e con i Visual C++ Redistributable 2015-2022 (x86 e x64) installati.

Messo tutto in fila — V5.0.15, te123, 256G, internet off, data 2021, VC++, COM bassa, firma disabilitata — la SMT Download mode finalmente parte.

Il colpo di scena che non ti aspetti#

Metto il telefono in EDL, l’MSM lo aggancia, e parte davvero: system.img to system_a, userdata.img to userdata, la barra che finalmente avanza dopo un giorno intero. Vado a farmi un caffè.

E il PC si spegne a metà — sospensione automatica. La cosa, l’unica cosa, che mi dovevo ricordare di disabilitare.

Il download EDL interrotto a metà scrittura del bootloader. Schermo nero. Non va più né in fastboot né in recovery.

Non è morto: il chip non si può brickare#

Momento gelo, poi la parte che voglio ricordare a chiunque legga in preda al panico: collego il telefono a Linux e guardo il dmesg.

usb 3-2: new high-speed USB device number 4
usb 3-2: idVendor=05c6, idProduct=9008
usb 3-2: Product: QUSB_BULK_CID:0404_SN:6CA17D78

Il telefono è vivo. Il PBL sta nella ROM del SoC — è immutabile, non lo puoi danneggiare. Con un bootloader corrotto il PBL fa auto-EDL da solo. Un device che enumera come 9008 è recuperabile al 100%, sempre. (L’error -71 iniziale? Di nuovo il cavo di merda — cambiato, enumera pulito.)

Il problema attuale è che il Sahara di questo auto-EDL è in stato d’errore persistente e i reset software non lo puliscono. Il “reset software” in questione, per la cronaca, è un USBDEVFS_RESET sul nodo — utile anche quando un edl ucciso con -9 lascia un claim orfano sull’interfaccia (Resource busy):

# forza la ri-enumerazione del device USB da userspace (ioctl 0x5514)
node=$(lsusb | awk '/05c6:9008/{gsub(":","",$4); printf "/dev/bus/usb/%s/%s",$2,$4}')
python3 -c "import os,fcntl; fd=os.open('$node',os.O_WRONLY); fcntl.ioctl(fd,0x5514,0); os.close(fd)"

Ma con un Sahara incastrato dal profondo non basta. La via dei professionisti è il test point: aprire il telefono e cortocircuitare due pad sulla scheda per forzare il PBL in un EDL pulito, bypassando il bootloader corrotto. Ho un lab di saldatura nuovo che aspetta esattamente questo.

Risorse & download (per non impazzire come me)#

Tutto quello che serve, in un posto solo. Occhio: i tool MSM sono bersaglio di malware — scaricali da fonti reputate, e su Windows verifica sempre di che si tratta prima di lanciarlo. Verifica sempre l’md5/sha256 dei firmware.

Tool di flashing (Linux, open source):

MSM Download Tool (Windows):

Driver Qualcomm HS-USB QDLoader 9008 (Windows):

Firmware OnePlus 7 (guacamoleb):

  • Il mio target: guacamoleb_14_H.01_211221 = HydrogenOS Android 11 CN (il “trio A11”: P.01/E.31/H.01). Fonti: Halab Tech (support.halabtech.com) e romdevelopers (romdevelopers.com). ⚠️ Il nome del file mente: domestic_11 è il ramo HydrogenOS, non la versione Android — verifica sempre ro.build.version.sdk nel build.prop (30/31 = A11/A12).

ROM custom + NetHunter (il traguardo):

Dove sono ora (e le lezioni, quelle vere)#

Il telefono mi aspetta — da aprire con calma, a casa e senza fretta: il vetro incollato e un cortocircuito su due pad da mezzo millimetro non sono roba da fare di corsa. Poi il piano è chiaro: apertura → reset totale scollegando la batteria → edl ripristina il bootloader → torno su Windows (con la sospensione disattivata, stavolta) per l’ultimo SMT download che chiude il flag → Android 11 pulito → e finalmente unlock + LineageOS + NetHunter.

Cosa mi porto a casa da questa maratona di 24 ore:

  • Il PBL Qualcomm è la tua rete di sicurezza. Finché il device enumera 9008, non è brickato. Ricordatelo quando lo schermo è nero e stai bestemmiando.
  • Una VM non è bare-metal. Per protocolli a basso livello timing-critici (Sahara), il proxy USB della virtualizzazione è un muro. Non è colpa tua.
  • Il muro che non cede è quasi sempre attaccato dal lato sbagliato. Il bug era il kernel. L’auth falliva per il file sbagliato. L’MSM crashava per la versione sbagliata.
  • La prudenza mal riposta fa più danni dell’imprudenza. Un timeout messo “per sicurezza” ha creato l’errore SMT. Una sospensione non disabilitata ha corrotto il bootloader. I dettagli stupidi sono quelli che ti fregano.

Per strada ho toccato con mano Sahara/Firehose, l’auth OnePlus (demacia/setprojmodel), MSM/OFP, qfil, una regressione del kernel, override dbus, USBDEVFS_RESET, driver signing di Windows, RPMB e secure boot. Un OnePlus 7 non si è mai fatto desiderare tanto.

Continua qui sotto — e stavolta boota davvero.

Aggiornamento — È VIVO (e su Android 11)#

Ripreso col saldatore pronto sul banco… che non è servito. Ma la parte finale ha comunque i suoi colpi di scena.

Lo smontaggio (e il vetro che si sacrifica)#

Il back dell’OnePlus 7 è una lastra di vetro incollata a caldo su tutto il perimetro. In teoria si scalda con phon o piastra, si infila un plettro sotto un angolo e si taglia l’adesivo lungo il bordo con pazienza. In pratica, senza piastra e con un vetro già stressato, viene via come viene via: a pezzi.

Il vetro posteriore che si sfalda durante l’apertura

Esteticamente un piccolo disastro — ma è una preoccupazione che non ho. Per un telefono che finirà a fare NetHunter, il vetro posteriore è irrilevante: anzi, senza cover si arriva meglio ai connettori e ai test point. Dietro al vetro c’è una staffa in plastica (che porta l’altoparlante e il coassiale d’antenna) tenuta da qualche vite Phillips — tolte quelle e sollevata con lo spudger, la motherboard è scoperta: al centro le schermature del SoC/PMIC e il modulo doppia camera, in basso la batteria.

E la batteria è la prima buona notizia: la BLP685 del 2019, dopo mesi di inutilizzo, è piatta e non gonfia. Su un telefono dimenticato in un cassetto era il rischio numero uno, e invece niente pancia.

La batteria BLP685 esposta

Aprire la batteria: il reset che serviva davvero#

Il pezzo cruciale è scollegare il connettore FPC della batteria. Si fa leva con lo spudger sotto il connettore — mai tirare i cavi — e viene su con un piccolo scatto. A batteria staccata il telefono è completamente senza alimentazione: è il power-cycle hardware vero, quello che nessun reset software è in grado di dare.

Motherboard scoperta, connettore batteria staccato

E proprio qui arriva la sorpresa che mi ha risparmiato il saldatore.

Il corto EDL che non è servito#

Ero pronto al test point: aprire, cortocircuitare i due pad sulla scheda e forzare il PBL in un EDL pulito, bypassando il bootloader corrotto. Il banco di saldatura era lì acceso, ad aspettare esattamente questo lavoro.

Non è servito. E il motivo vale la pena capirlo una volta per tutte: il corto EDL serve a chi in EDL non ci entra. Il mio telefono, col bootloader corrotto dal flash interrotto, ci andava da solo — in auto-EDL. Il PBL (il primo bootloader, che vive nella ROM del SoC ed è immutabile) quando trova un bootloader rotto ti butta direttamente in 9008. Quindi il problema non era entrare in EDL: era il Sahara in stato d’errore persistente, che i reset software (USBDEVFS_RESET e simili) non ripulivano.

E quel Sahara incastrato lo sblocca proprio lo stacco-batteria. Tolta corrente per un minuto buono, ricollegato il connettore, e al 9008 successivo l’handshake ripartiva pulito. Zero saldature, zero corti su pad da mezzo millimetro. Il saldatore è tornato nel cassetto — la prossima volta toccherà alla telecamera IoT, non a questo.

Lezione: il corto EDL è l’ultima spiaggia, non la prima. Se il device enumera già come 9008, prima di scaldare il saldatore prova lo stacco-batteria: un power-cycle hardware vero sblocca un Sahara che nessun reset via software riesce a toccare.

Il colpevole vero su Windows: triangolo giallo, Codice 52#

Torno sul PC Windows (il mio, non la macchina aziendale — mai flashare telefoni là) per l’MSM. Il device si presenta pulito: Qualcomm HS-USB QDLoader 9008 su COM5 — COM bassa, perfetta. Eppure l’MSM resta su “Waiting for device”.

QDLoader 9008 su COM5 con triangolo giallo — Codice 52

Il dettaglio è il triangolino giallo sull’icona: Codice 52 — impossibile verificare la firma digitale del driver. Non era la COM (quella era giusta): era il driver non firmato che Windows 11 rifiuta. Fix: disabilita l’imposizione firma driver (Avvio avanzato → Impostazioni di avvio → 7). Sparito il triangolo, l’MSM ha visto il device all’istante.

Lezione: con la QDLoader “installata ma gialla”, non perdere tempo sulle COM — è la firma driver. Codice 52 = disabilita l’enforcement.

MSM, stavolta fino in fondo#

V5.0.15, SMT mode, password te123, memoria 256G, internet off, data indietro. E soprattutto — la lezione che era costata il brick precedente — sospensione del PC disattivata. Start.

MSM che scrive le partizioni — SMT in corso

Stavolta la barra è arrivata in fondo senza che il PC si addormentasse a metà bootloader:

Download Complete — 725 secondi

Download Complete. 725 secondi. Il brick è storia.

Android 11. Il muro è caduto.#

Reboot, e parte il logo H₂OS — HydrogenOS cinese nativo, quello che boota senza crisi di region.

Boot HydrogenOS

E la riga che aspettavo da mesi, in About phone:

HydrogenOS 11.0.5.1.GM57 — Android 11

Hydrogen OS 11.0.5.1.GM57 → Android 11. Non 12. Che significa una cosa sola: il bug dell’abl A12 non c’è, e fastboot oem unlock — quello che mi sbatteva “technical reason” da mesi su ogni firmware A12 — ora deve passare.

Nota a margine (perché questo PC diventerà Ubuntu)#

Mentre l’MSM scriveva, il buon Windows 11 mi bombardava di finti popup McAfee (“virus critico!”, “antivirus scaduto!”) — notifiche push da un sito-truffa via Edge, su un PC che McAfee non ce l’ha nemmeno. Niente di grave, si tolgono revocando le notifiche del sito. Ma diciamo che la decisione di wippare Windows e metterci Ubuntu appena finito questo lavoro si è auto-motivata.

Le lezioni della seconda metà#

  • Il test point è l’ultima spiaggia, non la prima. Se il device fa già 9008, prova prima lo stacco-batteria.
  • QDLoader gialla = firma driver, non COM. Codice 52 → disabilita l’enforcement.
  • La sospensione del PC è il nemico numero uno. Disattivala prima di premere Start, non “tanto sto qui a guardare”.
  • Il firmware giusto era A11 cinese (H.01, HydrogenOS 11.0.5.1.GM57) — e il nome dei file mente sempre: conta solo ro.build.version.sdk.

Aggiornamento 2 — Il muro non c’era più: LineageOS, root e NetHunter#

Ripreso il telefono su Android 11, restava “solo” la discesa. Solo tra virgolette, perché tra me e la shell Kali si è messa di mezzo la cosa più stupida di tutta la storia: un cavo USB.

fastboot flashing unlock: stavolta passa#

OEM unlock e debug USB attivati nelle opzioni sviluppatore, adb reboot bootloader, e il comando che per mesi mi aveva risposto “technical reason”:

fastboot flashing unlock

Stavolta parte la schermata di conferma — Volume su per accettare, Power — e il telefono si wipa e riparte. Da qui in poi, a ogni boot, compare l’orange state:

<!> The boot loader is unlocked and software integrity
    cannot be guaranteed. PRESS POWER KEY TO PAUSE BOOT

Fastidioso ma sano: è il prezzo del bootloader aperto, e conferma nero su bianco che il bug A12 era l’unico ostacolo. Su Android 11 l’unlock passa e basta.

Il cavo che non c’era (e la dock che regge un colpo solo)#

Qui la storia diventa comica. Per flashare LineageOS mi serviva una connessione fastboot stabile, e mi sono ritrovato a provare nove cavi USB-C ↔ USB-C — tutti scartati dal kernel con lo stesso, secco error -71 (EPROTO): linee dati marginali o morte. Nove. L’unico link affidabile era un vecchio USB-A → USB-C, che però mi costringeva a passare per la dock del Dell.

E la dock ha un vizio preciso, che ho pagato a suon di flash falliti: dopo un fastboot reboot bootloader fresco, regge un solo trasferimento bulk. Il primo fastboot flash va; il secondo si pianta a metà, con timeout che non riesce nemmeno a uccidere l’ioctl USB in stato uninterruptible. La soluzione è brutale ma funziona:

# reboot bootloader PRIMA di OGNI singola partizione
fastboot reboot bootloader && fastboot flash dtbo   dtbo.img
fastboot reboot bootloader && fastboot flash vbmeta vbmeta.img --disable-verity --disable-verification
fastboot reboot bootloader && fastboot flash boot   boot.img          # slot _b

Un flash, un reboot. Un flash, un reboot. Lento, ma dtbo, vbmeta e boot sono entrati tutti.

Lezione: se ti serve un cavo dati serio, testalo. Ho poi scritto un validatore (b4d-usbcheck) che ti dice al volo se un cavo è dati OK, marginale o charge-only leggendo enumerazione, velocità negoziata e gli -71 nel kernel log. Nove cavi buttati fanno passare la voglia di indovinare.

C’è anche una nota su fastboot stesso: la versione 37.0.1 delle platform-tools, sull’OnePlus 7, risponde Requested download size is more than max allowed e non flasha nulla. Sceso a 34.0.5 — e tutto è filato. Le platform-tools recenti hanno cambiato il default della sparse-size in un modo che a questo bootloader non va giù.

Il sideload che tiene viva la dock#

Il colpo di fortuna è arrivato con lo adb sideload della zip LineageOS (1.3 GB). Contro ogni previsione la dock ha retto tutto il trasferimento — e il motivo è proprio l’opposto del problema di prima: un flusso attivo e continuo tiene su la connessione, mentre è l’idle tra un comando e l’altro che la fa cadere. Sideload completo, Install complete, reboot.

E dopo il logo che gira un po’ più a lungo del solito:

La home di LineageOS 23.2 sul OnePlus 7

LineageOS 23.2 alla home. Il telefono che per mesi mi aveva risposto “technical reason” adesso girava una ROM AOSP pulita. Da qui in poi si respira.

Root: Magisk patcha il boot#

La ricetta canonica, senza TWRP (che su questi A/B è più un intralcio): copio il boot.img di LineageOS sul telefono, lo do in pasto a Magisk → Install → Select and Patch a File, e lui sforna l’immagine patchata.

Magisk: boot.img patchato, All done!
- Stock boot image detected
- Patching ramdisk
- Pre-init storage partition: sda17
Output: /storage/emulated/0/Download/magisk_patched-30700_26Hsk.img
- All done!

Tiro giù il magisk_patched-30700_26Hsk.img, torno in fastboot e lo scrivo sullo slot _b (di nuovo con il rituale reboot-bootloader-prima-di-ogni-flash della dock). Reboot, e Magisk conferma: root attiva, ramdisk yes, Zygisk pronto. Magisk 30.7 (30700).

NetHunter: il modulo che si prende il kernel#

Ultimo passo, quello vero. NetHunter su LineageOS 23.2 arriva come modulo Magisk — niente flash manuale del kernel, ci pensa l’installer. Ho scelto la zip full (1.95 GB): dentro c’è anche il rootfs Kali completo, così il chroot si installa già durante il modulo.

Magisk → Modules → Install from storage → la zip NetHunter. E parte il banner che vale tutta la fatica:

L'installer NetHunter dentro Magisk: banner ASCII e installazione delle app
###########  NetHunter  ###########
* NetHunter (Magisk)
- Current boot slot: _b
- Device is system-as-root
* Installing apps (via system):
** Installing NetHunter.apk
** Installing NetHunterTerminal.apk
** Installing NetHunterKeX.apk
** Installing NetHunterStore.apk
* Setting up BusyBox ...

Il modulo sostituisce il kernel stock con quello NetHunter (4.14.357-v0lk3n-SM8150_OP7Serie-LOS_23.2) — quello con injection Wi-Fi, driver per le schede esterne e tutto l’armamentario — e poi si mette a scompattare il rootfs Kali (“Extracting Kali rootfs — this may take up to 25 minutes”, e non scherza).

Un dettaglio che vale oro: quando l’installer flasha il kernel NetHunter con AnyKernel3, si accorge da solo della root già presente — “Magisk detected! Patching kernel so reflashing Magisk” — e reinietta Magisk nel nuovo boot. Kernel NetHunter e root, senza doverla rifare a mano.

Kali NetHunter is now installed — il modulo ha finito
- A/B device Detected. Current slot: _b
- Magisk detected! Patching kernel so reflashing Magisk
- Boot image patching complete
* Installing Kali chroot
  - Found Kali chroot to be installed: kalifs-full-arm64
  - Kali arm64 full chroot installed successfully!

######  Kali NetHunter is now installed!  ######
- Done

Kali NetHunter is now installed. Chroot full incluso, root preservata, un pulsante Reboot blu che aspetta solo di essere premuto.

Reboot: il drago si sveglia#

Premuto Reboot, il primo avvio sul kernel nuovo si è preso qualche secondo in più — poi la lockscreen, con uno sfondo che non lasciava dubbi:

La home di LineageOS con lo sfondo Kali NetHunter dopo il reboot sul kernel v0lk3n

Il drago Kali NetHunter come wallpaper, e — la conferma che conta davvero — il kernel giusto in esecuzione:

$ uname -a
Linux localhost 4.14.357-v0lk3n-SM8150_OP7Serie-LOS_23.2+ #9 SMP PREEMPT aarch64

v0lk3n. Non più il kernel stock di LineageOS: quello NetHunter, con l’injection Wi-Fi e i driver per le schede esterne compilati dentro.

root@kali — la shell che aspettavo da mesi#

NetHunter Store aggiornato, permessi root concessi all’app dal popup Magisk, Chroot Manager che vede il kalifs-full già installato. Poi il NetHunter Terminal, sessione Kali, e finalmente il prompt:

La prima shell Kali root sul OnePlus 7
┌──(root㉿kali)-[/home]
└─# uname -a
Linux kali 4.14.357-v0lk3n-SM8150_OP7Serie-LOS_23.2+ ... aarch64 GNU/Linux

┌──(root㉿kali)-[/home]
└─# cat /etc/os-release
PRETTY_NAME="Kali GNU/Linux Rolling"
VERSION_ID="2026.2"
VERSION_CODENAME=kali-rolling

┌──(root㉿kali)-[/home]
└─# whoami
root

Kali GNU/Linux Rolling 2026.2, uid 0, dentro un chroot arm64 full, su un telefono. Il progetto che “doveva essere una serata” è chiuso.

Il percorso completo, dal brick alla shell#

EDL (auto-9008) → stacco-batteria (reset Sahara) → MSM → HydrogenOS A11
→ fastboot flashing unlock → LineageOS 23.2 → Magisk → modulo NetHunter → root@kali

Le lezioni della terza metà (perché in questo progetto le metà sono tre):

  • Un cavo USB può costarti una serata. error -71 = linee dati morte; testa i cavi, non indovinare.
  • La dock regge un colpo per reboot — a meno che il flusso non sia continuo (sideload). Reboot-bootloader prima di ogni flash.
  • fastboot 34.0.5, non 37.x, su questo bootloader. Il tool più nuovo non è sempre quello giusto.
  • NetHunter è un modulo Magisk, non un flash manuale: si prende lui il kernel. La zip full porta anche il chroot.

Dal “cannot be unlocked for technical reason” di mesi fa a root@kali sul palmo di una mano. Ci abbiamo messo — tra un firmware cinese, uno stacco-batteria, nove cavi buttati e una dock dispettosa — molto più di una serata. Ma il muro è caduto, e il drago respira.

Da qui in poi è tutto software: kex per il desktop Kali completo in VNC, i metapackage per l’arsenale su misura, e la prima scheda Wi-Fi esterna in monitor mode da telefono. Ma quella è un’altra storia — e stavolta, finalmente, la parte facile.