
Root temporal (uid 0) en un ASUS Zenfone 9 con bootloader bloqueado mediante CVE-2025-21479 + una fuga de dirección física basada en perf. GPLv3.
Código de investigación y notas para obtener root temporal (uid 0) en un ASUS Zenfone 9 (AI2202) cuyo
bootloader no puede desbloquearse — porque ASUS descontinuó su herramienta de desbloqueo, eliminó el
conmutador de desbloqueo OEM de las opciones de desarrollador y rechaza tajantemente fastboot oem unlock / flashing unlock en el
firmware actual.
Esto no es un desbloqueo de bootloader y no flashea nada. Es un root en tiempo de ejecución: una cadena de primitivas del lado del dispositivo que termina con el proceso llamador poseyendo credenciales de root. Un reinicio lo borra.
Verificado en: ASUS Zenfone 9 (AI2202), Android 14, build 34.0304.2004.145, SPL 2024-07-05,
kernel 5.10.205-android12-9-00029-g3f12df86bfdb-ab11799032, SM8475 / Adreno 730, bootloader bloqueado.
Autorización / alcance. Todo aquí se ejecuta contra un dispositivo que el operador posee, sobre una conexión ADB autorizada. No se ataca ningún servidor de proveedor, no se elude ninguna clave de firma ni token de desbloqueo, y no se escribe nada en ninguna partición. El teléfono usado para desarrollo es uno de repuesto, respaldado y mantenido offline. Prueba en hardware que poseas y puedas permitirte perder.
El resultado es uid 0 en el contexto SELinux u:r:kernel:s0 (hereda el SID de init_cred), es decir,
efectivamente sin restricciones. SELinux debe estar en Permissive para este paso — bajo Enforcing el proceso es
matado en su lugar, porque el intercambio de credenciales elude el hook de SELinux.
Establece ZF9_SERIAL con el serial de tu dispositivo primero (todos los scripts lo leen):
export ZF9_SERIAL=<your-device-serial>
# 0. one-time: build and push the device binaries (needs an Android NDK)
# see scripts/ for the exact clang invocations used
adb push cheese_pa call_capset /data/local/tmp/
# 1. full cycle: SELinux -> permissive, patch, run a command as root, restore everything
scripts/root-now.sh id
scripts/root-now.sh sh # root shell
El script siempre restaura el texto original del kernel y el estado de SELinux (protegido con trap), y verifica la restauración mediante lectura de vuelta.
uid=0(root) gid=0(root) context=u:r:kernel:s0,
capset(NULL,NULL) -> 0.gpu fault threshold exceeded 3 faults in 3000 msecs), tras lo cual más comandos fallan con
EPERM. El éxito del parcheo observado varía entre 13/13 y 2/13 dwords entre ejecuciones.screenrecord en ejecución. Los scripts inician
su propia carga para cada operación (las lecturas también compiten), pero nunca ejecutan las interioridades de estas herramientas en crudo.capset puede ejecutar basura). Los scripts
escriben la instrucción de entrada al final, verifican cada dword y restauran en caso de fallo — pero si una ejecución se degrada,
reinicia antes de reintentar.Reglas de seguridad que vale la pena mantener: leer antes de cada escritura, restaurar siempre lo que parcheas, nunca dejar
una entrada de tabla de páginas inyectada activa, no tocar memoria física segura/TZ (es fatal), y reiniciar para
recuperar un estado degradado. ROADMAP.md tiene la lista completa de trampas con la evidencia detrás de cada una.
ROADMAP.md durable handoff: verified constants, procedure, gotchas, open paths
STATUS.md current state + verification receipts
src/ pa_leak.{c,h} · cheese.c · cheese_pa.c (workhorse: PROBE/POKE/SELFTEST/ROOT modes)
call_capset.c · host_kallsyms.c (offline symbol resolver)
scripts/ root-now.sh · patch-dwords.sh · demo-root.sh · verify-backup.sh
tools/ btf_offsets.py
Las imágenes de firmware, paquetes OTA y registros del dispositivo deliberadamente no se incluyen en el commit (ver .gitignore).
GPLv3 — ver LICENSE.
| Etapa | Qué hace | Dónde |
|---|
| 1 | CVE-2025-21479 (Adreno KGSL): un paquete SDS se clasifica erróneamente como un paquete de ringbuffer, permitiendo a userland emitir CP_SMMU_TABLE_UPDATE y apuntar el TTBR0 de la GPU a una dirección física elegida por el atacante | src/cheese.c |
| 2 | Fuga de dirección física: perf_event_paranoid = -1 en esta build, así que un watchpoint de hardware sobre una página que poseemos devuelve PERF_SAMPLE_PHYS_ADDR — la dirección física de esa página. Reemplaza la fuga de pagemap en la que confiaba upstream (los PFN están a cero aquí) | src/pa_leak.{c,h} |
| 3 | Lectura/escritura física arbitraria: construir la tabla de páginas falsa en una dirección física conocida (de la etapa 2), así que sin lotería de spray y sin recorridos salvajes | src/cheese_pa.c |
| 4 | Root: resolver símbolos del kernel offline desde la imagen del firmware, derivar el slide de KASLR en el dispositivo, parchear __do_sys_capset con un stub commit_creds(&init_cred), llamar a capset() | src/call_capset.c, scripts/ |