
Investigación de exploit del kernel que logra root temporal en Amazon Fire 7 (Fire OS 7.3.3.1) mediante el use-after-free del JIT de Mali kbase CVE-2022-38181, con una cadena de sobrescritura de modprobe_path.
Proyecto asistido por IA. Esta investigación, el desarrollo del exploit y la documentación fueron producidos con asistencia de IA utilizando los modelos GLM-5.3 y DeepSeek V4.1 Flash.
Investigación de exploit de root para el Amazon Fire 7 9.ª gen (mustang, MT8163, Mali-T720) en el firmware final — Fire OS 7.3.3.1, PS7331.4463N, kernel 4.9.117 (compilado 2025-05-03, SPL 2024-08-01).
Objetivo: LineageOS. La ruta del bootloader está muerta en esta unidad (bootrom parcheada — solo preloader vía cortocircuito CMD), por lo que la única ruta restante es un exploit de kernel por software.
nix-shell -p android-tools --run './run.sh' # add -p zig too if no zig
Si tiene éxito:```
/data/metrics/su id # run a command as root
/data/metrics/su # interactive root shell
El reclaim gana aproximadamente 1 arranque de cada 3 y una pérdida provoca un panic/reinicio de la tablet;
run.sh simplemente espera al reinicio y reintenta. SELinux se fuerza a Permissive como
parte del exploit, por lo que root es solo en tiempo de ejecución — un reinicio restaura el estado original y
vuelves a ejecutar run.sh.
Los binarios precompilados st3 y su (armv7 estáticos) están incluidos, así que no se necesita toolchain
para ejecutar. ./run.sh --build los recompila desde poc/*.c si tienes zig.
GhostLock (abajo) está aparcado: la variante BUG_ON rtmutex de MTK + la ausencia de divulgación de direcciones del kernel desde el shell = callejón sin salida arquitectónico en esta compilación (sesiones 2-4). El UAF del JIT de kbase fue re-diagnosticado (el "panic incondicional" del destroy-worker era la desreferencia de JIT_FREE, registro perdido por la muerte de adbd a mitad del panic) y la etapa 2 ahora está probada por oráculo — ver la sección SESIÓN 5.
UAF de pila futex-PI en remove_waiter() de rtmutex (divulgación de NebuSec 2026-07, corrección 3bfdc63936dd
aplicada 2026-04). Rango vulnerable 2.6.39–7.1 → nuestro 4.9.117 (mayo 2025) está afectado.
Verificado en nuestra compilación exacta:
CONFIG_FUTEX=y, rtmutex compilado, bug presente textualmente:
rtmutex.c:1108-1111 usa current->pi_lock/current->pi_blocked_on (debería ser
waiter->task); sitio de llamada con bug rtmutex.c:1723 (ruta de error de rt_mutex_start_proxy_lock)WAIT_REQUEUE_PI/CMP_REQUEUE_PI), sin nodo de dispositivo,
nada restringido por SELinux — los obstáculos fatales de la ruta kbase no existen aquísched_setattr → __sched_setscheduler → rt_mutex_adjust_pi(p) en
sched/core.c:4706 — desreferencia obsoleto ✓exp32/main.crt_waiter vs área fd_set de do_sys_select — desensamblar
nuestro vmlinux (do_sys_select stack_fds vs frame de futex_wait_requeue_pi), exponer
STAMP_NFDS/STAMP_WAITER_OFF como parámetros ajustablesFailed critical init step 3/dev/mali0 world-RW + SELinux gpu_device, kbase r26p0-01rel0mustang, Fire OS 7.3.3.1 PS7331.4463N/00315758630404.9.117-g08fe75b-dirty, compilado Sat May 3 01:25:15 UTC 2025 (Linaro GCC 6.3-2017.05)/dev/kb, /dev/dkb (particiones de respaldo del kernel de Amazon) root:drmrpc 0660 — bloqueadasmali_kbase r26p0-01rel0 (Midgard, Mali-T720), dentro del rango afectado de NVD r4p0–r31p0mali_kbase_mem.c:2721 kbase_jit_destroy_worker libera la región, nunca limpia kctx->jit_alloc[id]mali_kbase_softjobs.c:1270 kbase_jit_free_finish desreferencia el jit_alloc[ids[j]] obsoletomali_kbase_mem.c:3138 kbase_jit_backing_lost → ruta de destrucción (se dispara durante el reclaim)0xc0008000 / PA 0x40080000)ARM_SW_DOMAIN_PAN → ret2usr viable; CONFIG_PANIC_ON_OOPS=y (intentos fallidos = reinicio)SLAB_FREELIST_RANDOM/HARDENED, sin CONFIG_USER_NS/USERFAULTFD/NF_TABLESCONFIG_MODULES=y, sin STATIC_USERMODEHELPER → sobrescritura de modprobe_path = root_IOC_TYPE 0x80)MEM_ALLOC es de 32 bytes (in tiene 4 × u64 incl. extent)BASE_MEM_PROT_GPU_RD|WR (bits 2|3), no los legacy R|Wmmap(fd, offset=3<<12, PROT_NONE)JOB_SUBMIT debe ser igual a sizeof(base_jd_atom_v2) = 48 (base_jd_prio/base_jd_dep_type son typedefs u8)MEM_JIT_INIT (nr 14, struct v2), alloc/free son soft jobs vía JOB_SUBMIT
(BASE_JD_REQ_SOFT_JIT_ALLOC=0x209, ...FREE=0x20a; =ptr de usuario, =count)El panic ocurre durante la propia expulsión (evictable_reclaim_scan_objects → backing_lost →
destroy worker) — las referencias colgantes (jit_alloc[], lista de evict) se recorren antes de que
enviemos JIT_FREE. La etapa 2 debe ganar la carrera: reasignar el kbase_va_region liberado con nuestro
propio spray de MEM_ALLOC mientras la presión sigue en curso.
poc/stage2.c — exploit de etapa 2 (modos: step/uaf/spstep/spfree/spray/keys)
— spray 700 = ejecución completa del oráculo; sobrevive y se pausa (matar para limpiar)poc/mustang_jit_uaf.c — PoC de etapa 1 (modos: jit N / control N / pressure N)poc/build.sh — compilación cruzada con zig (musl estático armv7)kernel/vmlinux — símbolos recuperados de la compilación exacta de la OTA (vmlinux-to-elf)nix-shell -p zig --run 'zig cc -target arm-linux-musleabihf -static -O2 -o juaf poc/mustang_jit_uaf.c' adb push juaf /data/local/tmp/juaf && adb shell chmod 755 /data/local/tmp/juaf adb shell /data/local/tmp/juaf jit 700 # full trigger (~reboots device) adb shell /data/local/tmp/juaf control 700 # no-JIT control adb shell /data/local/tmp/juaf pressure 700 # raw memory-pressure control
## Referencias
- Aviso GHSL-2022-054: https://securitylab.github.com/advisories/GHSL-2022-054_Arm_Mali/
- Análisis del exploit de Mo para Pixel 6: https://github.blog/2023-01-23-pwning-the-all-google-phone-with-a-non-google-bug/
- Precedente del Fire HD 10 (trona), misma familia de bugs: ericpardee.github.io/fire-hd-ownership
- Portal OSS de Amazon: amazon.com gp/help/customer/display.html nodeId=200203720
- Hilo de desbloqueo en XDA (muerto para esta rev de hw): xdaforums.com/t/fire-7-2019-mustang-unbrick-downgrade-unlock-root.3944365/
## Apéndice de la sesión 3 (sondeo profundo de sellos de syscall)
Profundidades de copia-origen medidas (absolutas vs sp0 en entrada de syscall; el waiter abarca -0x1d8..-0x1a8):
- sendto sockaddr @ -0xc4 | process_vm iov @ -0x104 | recvmsg iov @ -0x11c
- sendmsg iov @ -0x12c | recvmmsg iov @ -0x154 | select fds @ -0x174
- pselect6 fds @ -0x19c | **sendmmsg iov @ -0x1bc (MEJOR — 0x1c antes de waiter+0x00)**
- poll entries @ -0x3e0 (completamente por debajo; lado equivocado)
Descartado en esta sesión:
- cadena io_submit demasiado superficial (~-0x130) | semtimedop: CONFIG_SYSVIPC=n (stub)
- configfs montado pero CERO subsistemas registrados (sin objetivos mkdir)
- /sys/kernel/debug, /config: denegado por SELinux para shell
- /proc/sys/kernel: getdents funciona (29 entradas listadas), solo pid_max es ABRIBLE;
lecturas de kptr_restrict/hotplug/hostname/domainname todas denegadas
- El valor de escritura es SIEMPRE waiter+0 (dirección de pila del kernel, ejecutable, shellcode en
+0x1c): rb_link_node *link = node, insert_color escribe parent-color — no
es posible una variante de valor controlado sin estampado de campos del árbol (hueco -0x1d8..-0x1bc)
- Campos de despacho de doble desreferencia leen *(waiter+0)=1, *(waiter+4/+8)=0 — BLX 1/0
(nf_hooks, net_families, inet[6]_protos, seq_file->op todos muertos)
- timer_list.function@+0xc y work_struct.func@+0xc leerían *(waiter+0xc) =
pi_tree self-ptr = waiter+0xc EJECUTABLE — pero ninguna ruta encola waiter+0 como
timer/work (corrupción de enlace / sin fuentes de encolado indirecto)
- Raíz-del-árbol-no-cero (slot de handler sysctl) = pointer-chase determinista a través de
.text como rb-tree; termina en una palabra cero — simulable offline, pero aterrizar
en un slot escribible útil es implausible
Pistas restantes para la sesión 4:
1. rutas profundas de ioctl: copia de ifreq en dev_ioctl (40B de datos de usuario) — medir
profundidad de la cadena SyS_ioctl→sock_ioctl→dev_ioctl vs -0x1d8
2. Cualquier otra copia 0x1c más profunda que sendmmsg (nada encontrado aún)
3. Si la caza de superficie-de-estampado falla: reconsiderar construcciones encadenadas por recorrido o
cazar clases de slots escribibles-llamados-con-cero aún no enumeradas
## SESIÓN 4 — LOS DOS AVANCES
### 1. El rtmutex_common.h de MTK es todo el misterio
MTK reemplazó el rt_mutex_top_waiter seguro frente a NULL de upstream por:```c
w = rb_entry(lock->waiters_leftmost, struct rt_mutex_waiter, tree_entry);
BUG_ON(w->lock != lock); // compiled to: ldr sb,[lock+8]; ldr r3,[sb+0x1c]; cmp; bne→udf#0x12
SIN COMPROBACIÓN DE NULL + BUG_ON. Cada ancla todo-ceros muere en *(NULL+0x1c); las anclas basura mueren en el udf. EL RECORRIDO REQUIERE: lock->waiters_leftmost (lock+8) debe apuntar a un waiter falso W (escribible) con W->lock (+0x1c) == lock.
Flujo del recorrido completamente mapeado (rt_mutex_adjust_prio_chain @ 0xc0189a58):
/proc/self/task//stat campo 28 (kstkesp) devuelve el SP REAL del kernel para hilos bloqueados en syscall desde contexto de SHELL (verificado: valores distintos de cero observados).
El recorrido se dispara de forma determinista (sonda de dead-lock: crash cada vez, código limpio). La escritura no puede aterrizar debido a una coincidencia triple de endurecimiento del kernel:
La ventana de sello (waiter+0x1c..0x5b) es la única memoria con contenido controlado+conocido, pero su DIRECCIÓN es la incógnita que necesitamos. Las construcciones autorreferenciales todas requieren sellar una dirección del kernel como constante — circular sin una fuga.
Su kernel 5.4 tiene rtmutex_top_waiter UPSTREAM (seguro ante NULL: if (!leftmost) return NULL) — las anclas de árbol vacío sobreviven, su escritura aterrizó en null_fops
(escribible en su kernel). Incluso ELLOS están atascados en el dispatch ("ioctl reboot").
El árbol MTK 4.9.117 de Mustang tiene la variante BUG_ON — las tablets Fire OS 8
(GhostLock-5.10) tuvieron éxito porque sus kernels 5.10 son estilo upstream.
Probado y muerto desde el dominio shell:
CONCLUSIÓN: GhostLock en mustang requiere una divulgación de dirección del kernel que este kernel no expone al dominio shell. El lock falso autorreferencial no puede construirse sin ella.
(a) Molienda determinista por arranque: reiniciar → calibrar dirección de pila vía oráculo de crash (~20-30 reinicios), verificar reproducibilidad. Apuesta arriesgada — la asignación de pila de hilo en arranque tardío probablemente no es estable. (b) PIVOTAR de vuelta a kbase CVE-2022-38181 con los activos acumulados: vmlinux de build exacta + código fuente completo + toolchain + disciplina de trazas O_SYNC + conocimiento profundo de 4.9. El bloqueador original (panic del destroy-worker durante la expulsión JIT) es un problema de timing de spray, ahora mejor comprendido. (c) Parar en un honesto ~45%: trigger probado, recorrido mapeado hasta la instrucción, escritura bloqueada por BUG_ON de MTK + sin fuga.
Recomendado: (b) — el bug de kbase está verificado-presente en este código fuente exacto, tenía un trigger funcional, y su bloqueador es mecánico, no arquitectónico.
El modo step (alloc id=1 → DONT_NEED → presión de 700MB → MEM_QUERY, SIN free)
sobrevive: query=-1 (región liberada por el destroy worker, rbtree-limpio).
La ruta del worker es byte-idéntica al flujo legal de JIT_FREE-bajo-presión.
El crash de la sesión 1 era siempre la desreferencia colgante de JIT_FREE; su línea de log se
perdió porque el panic mata adbd a mitad del flush. Verificado dos veces más con el modo uaf
(free desnudo → panic, mismo corte de log). El flujo GHSL-2022-054 está totalmente vivo en esta build.
kbase_jit_free(kctx, reg) @ 0xc058495c con reg falso totalmente controlado:
reg->cpu_alloc NULL → tamaño respaldado 0 → bloque de recorte omitido (0xc0584978)kctx+0x147dd (byte) + kctx+0x147de+bin_id (byte)mark_reclaim(reg->gpu_alloc) @ 0xc059b158: cadena
K=*(gpu_alloc+0x38) → *(K+0x1429c)==0 omite mm-atomics →
atomic_sub nents@K+0x141c8, D=*(K+4) → atomic_sub nents@D+0x538.
Con nents=0 todas las escrituras son stores no-op (strex del mismo valor).reg->flags |= 0x100000 (escritura en el falso, benigna)list_add(gpu_alloc->evict_node, &kctx->evict_list): cabeza de evict_list
@ kctx+0x1427c; escribe en gpu_alloc+0x18/0x1c (debe ser escribible)r3=*(reg+0x3c) prev, r2=*(reg+0x38) next
→ *(next+4)=prev; *(prev+0)=next — dos escrituras-arbitrarias-qué-dónde,
luego relink de reg+0x38 en jit_pool_head @ kctx+0x148e89 candidatos; S=0xc118b7ec (datos xfrm, inactivo en este dispositivo):
*(S+8)=0 (nents), *(S+0x18)=S+0x18 (evict_node vacío → sin WARN),
K=*(S+0x38)=0xc118b820 → *(K+0x1429c)=0, K/D+0x141c8/+0x538 todos en
datos escribibles. Objetivos del oráculo preparados: init_uts_ns.name.nodename=0xc110d561
("(none)", legible vía uname), scratch P=0xc118bd58 (ceros de xfrm).
Evitar S=0xc111cba4 (adyacente a tracepoint). CONFIG_DEBUG_RODATA=y → todos los objetivos
de escritura deben estar en .data/.bss (bss 0xc11d9000-0xc12d9000).
add_key (CONFIG_KEYS=y): denegado por SELinux para shell. Muerto.setxattr: kvmalloc(96)+copy_from_user ocurre ANTES de la
comprobación de SELinux → la danza de alloc es a prueba de SELinux incluso cuando la llamada falla;
transitorio (liberado al final de la syscall), los bytes persisten en +4..95 (el ptr de freelist
sobrescribe +0..3 = rblink, no usado por kbase_jit_free)Ejecución spray 700 2026-09-11: 5895 regiones rociadas durante la presión, JIT_FREE
sobre id=1 colgante completado sobre una región reclamada, luego
JIT_ALLOC(0x40, bin 0) recorrió jit_pool_head y devolvió la VA de la región rociada
#4251 (0x142701000) — la región exacta que consumió el puntero colgante.
Kill del proceso después: teardown de kctx limpio, sin crash. Etapa 2 completa:
redirección UAF determinista con tipo de objeto + contenido controlados.
La reclamación de tipo región da supervivencia a la danza legal pero jit_node está INIT'd a sí mismo → sin primitiva de unlink. Se necesitan bytes crudos en +0x38/+0x3c:
*(N+4)=P con P=página de shellcode de userspace (¡sin PAN!) —
candidatos: fops const son .rodata (DEBUG_RODATA) → apuntar a fn ptr no-const
en .data, o cabeza de lista formats de binfmt, o proc_handler de sysctl
(verificar escribibilidad de la tabla). fallback: modprobe_path vía escrituras
encadenadas por bytes (los valores deben ser direcciones escribibles — usar objetivos con forma de puntero)poc/stage3.c):
kern_table[pid_max].proc_handler @ 0xc1113f40 (escribible
.data, verificado vía escaneo de punteros a strings + handler == proc_dointvec_minmax)b +8; W2 escribe N en P = campo handler)read /proc/sys/kernel/pid_max
(legible desde shell); corre en contexto de su propia tarea → los creds se aplican a nosotrosinotify_handle_event hace kmalloc de name_len+0x1d,
bytes del nombre (totalmente controlados, restricción sin NUL/slash) en event+0x1c;
name_len=60 → kmalloc-96; encolado → retenido; 4 instancias → 4 eventos por
rename; SELinux-OK desde shell. Falso rediseñado sin NUL: cpu_alloc
apunta a S (nents@S+8 = 0 → misma semántica que NULL)| variante | resultado |
|---|
Hipótesis de trabajo: carrera de basura de tormenta — entre el destroy worker liberando el slot (a mitad de tormenta) y el kill-child/silencio, la actividad de reclamación residual toma el único-slot-libre del slab víctima con bytes no-payload. El spray de regiones (etapa 2) gana porque asigna continuamente DURANTE la tormenta; los renames no pueden.
iso): timing idéntico, final con REGIONES commit-0
current->mm es el de un hilo del kernel → el elegante
diseño de "apuntar los punteros del falso a nuestro propio mmap de userspace" (¡sin PAN!)
FALLA de forma no determinista. 5/5 crashes con un falso por lo demás perfecto.K=*(S+0x38) legible, *(K+0x1429c)==0 en runtime
(omite mm-atomics), K+0x141c8 escribible, D=*(K+4) → D+0x538
escribible, nents *(S+8) preferiblemente 0. Los 9 candidatos S offline fueron
validados contra bytes de ARCHIVO — la deriva en runtime (init de xfrm/tracepoint)
los deja sin verificar. Una cadena incorrecta = crash = reinicio (~3 min de ciclo).A. Falso con spray de physmap (ret2dir, clásico arm32 sin-PAN): rociar ~450MB de páginas de usuario conteniendo cada una el patrón falso preparado para UNA dirección adivinada G (G&0xfff = 0x141 para bytes de nombre sin NUL; S=G; K=G-0x141b4 para que K+4 caiga en página; K+0x141c8/+0x1429c → G+0x10/+0xd4 en página; D=G+0x300; los stores sub-0 extraviados golpean RAM mapeada aleatoria - inofensivo con nents=0). El spray sirve también como presión de expulsión (anon sucio = no expulsable → solo se necesitan ~100-200MB extra). Probabilidades ≈ 45% (acierto de página) × ~50% (palabra K+0x1429c ajena es cero... si K+0x1429c se mantiene en página según el layout anterior, probabilidades = solo acierto de página). Fallo = crash = reinicio, reintentar. B. Fuerza bruta sobre los 9 candidatos S estáticos (xfrm 0xc118b7ec primero, adyacente a tracepoint 0xc111cba4 segundo...): 1 reinicio por candidato, payload de oráculo benigno primero, arma al acierto. C. G exacta basada en pagemap (muerta: PFNs enmascarados) — no revisitar.
/tmp/opencode/ksrc2/kernel/mediatek/mt8163/4.9/ (arch/arm incl.
mustang.dtsi, mm/, fs/eventpoll.c, fs/notify) desde ksrc/platform.tar.
Hallazgos:
physmap_retouch() tras el kill (falla todas las páginas del spray
de vuelta antes del trailing/deref)Las regiones reclaman: 3/3. Eventos: 0/~12 intentos incluyendo 28K asignaciones exclusivas con ventaja inicial y fake correcto por construcción. Si los eventos reclaman con p_hit(G)≈0.35, cinco fallos de pmap ≈ 11.6% — posible pero ahora improbable (~10-15%). O los eventos estructuralmente no pueden tomar este slot (razón desconocida — misma caché, mismo contexto, mismo timing) o nuestras conjeturas de G fallan sistemáticamente (sesgo en el límite de highmem, colocación del asignador).
vmalloc=496M slub_max_order=0 slub_debug=O loglevel=8 initcall_debug
Las regiones toman el slot de víctima 3/3; los eventos 0/13. Misma caché (probado a nivel de código fuente + disasm), mismo contexto de proceso, mismas cpus fijadas, mismo timing post-kill, miles de asignaciones con ventaja inicial exclusiva. Mecanismo desconocido. Sospechosos restantes: correlación de tasa/frecuencia de asignación con la rotación de lista parcial (ioctls de región ~1ms de diferencia vs renombrados de eventos ~100µs de diferencia — ¿direcciones opuestas?), o detalles del orden de la freelist de SLUB bajo slub_max_order=0 que favorecen... poco claro.
[+] uname.nodename="X?lhost" (was "localhost") <<< ORACLE HIT
**La cadena completa de bytes en crudo funciona en el dispositivo real**: el evento reclama el
slot de región liberado → kbase_jit_free desreferencia nuestro falso (S=página physmap del
spray kbm, G=0xc154b2a4) → el unlink ejecuta nuestras dos escrituras.
Verificado repetidamente con el payload benigno (escritura de nodename).
### Cadena de descubrimientos de la sesión
1. las páginas kbm eran GFP_HIGHUSER → HIGHMEM, invisibles para physmap
(mali_kbase_mem_pool.c:164!) — corregido mediante el desbordamiento de zonelist: 260 × 2MB
de spray > highmem-free → el excedente aterriza en ZONE_NORMAL (physmap)
2. Los primeros intentos de arma fallaron: el objetivo W1 N+4 era una página USER —
el unlink se ejecuta en ctx de kworker (sin mm) → fallo. Corregido incrustando el
shellcode de ring-0 DENTRO del patrón physmap en page+0x600 (direct map RWX en
arm32 non-LPAE) — código residente en kernel, sin necesidad de ret2usr
3. Bug de offset en ctl_table: proc_handler está en entry+0x14, no en +0x18 (el
escaneo original lo tenía bien; mi define estaba mal) — estaba escribiendo extra1
4. Regresión por sobrepresión encontrada y revertida: el presupuesto de kid 20×100MB +
trailing de 10s rompía el reclaim; la configuración que funciona es 2 kids/200MB +
5s/+3200 trailing (el benigno acertó 1/1 tras revertir)
5. **diag2: W2 → &pid_max global (0xc1114d7c) → la lectura devuelve nuestro valor
(-1055861411 = 0xc110d55d como int32) — escritura + lectura de vuelta PROBADA**
### El misterio restante (a un experimento de cerrarse)
diag1 con la dirección CORRECTA del handler (0xc1113f3c): W1 se dispara (nodename
cambia), W2 debió ejecutarse (siguiente instrucción) — sin embargo las lecturas de pid_max
siguen devolviendo valores limpios → el campo handler que escribimos no es el que
el inode despacha. diag3 (en cola; necesita un boot con acierto): W2 →
campo entry->data (0xc1113f2c) apuntando a nodename — si la lectura entonces
muestra los bytes de nodename como int, nuestra entry ESTÁ viva y solo el offset del handler
está de algún modo mal; si no se ve afectada, el inode usa una copia shadow de la tabla
y cazamos la viva.
### Realidad de la tasa de aciertos
Moneda al aire por boot (~25-40%), agrupados; varios boots con fallo seguro/crash
seguidos es normal. Aproximadamente 1 de cada 3-4 boots es un acierto. Mantén la configuración
que funciona EXACTAMENTE (2 kids, 5s trailing, kbm de 260 regiones, G=0xc154b2a4).
### TODO de la sesión 9
1. Completar diag3 en un boot con acierto (reintentar hasta "W1 FIRED")
2. Si la entry está viva: re-verificar empíricamente el offset del handler (escribir
la dirección de proc_dostring como handler vía... N debe ser igual a un valor
útil — usa el unlink para escribir entry->data en su lugar y pivotar: p. ej.,
data=selinux_enforcing-adyacente...)
3. Si es tabla shadow: localizar la viva — kallsyms no tiene símbolos de datos;
candidatos: escanear el comportamiento de /proc/sys, o encontrar una segunda región
ctl_table mediante el patrón de lista de headers en .data (entries con stride de 0x20
con handler=proc_dointvec_minmax y maxlen=4 — enumerar TODAS y
hacer diag-write en cada una)
4. Clase de objetivo alternativa que evite el dispatch por completo: punteros a función
en .data llamados desde rutas alcanzables por shell (requiere auditoría)
5. La primitiva de escritura en sí está HECHA — cualquier objetivo fiable de dirección
de kernel ahora basta para root
## SESIÓN 9 — nf-WEAPON: reclaim+unlink+G-hit PROBADO EN EL ARMA; solo queda el recorrido del hook
### El nuevo diseño del trigger (reemplaza por completo la ruta del handler sysctl)
La idea del usuario traducida a memoria de kernel: sin archivo SUID (el sistema es
dm-verity RO; la primitiva escribe RAM de kernel). En su lugar: **hook netfilter falso**.
Este kernel tiene el backport común de Android de la NUEVA
API nf_hook_entries, pero implementada como LISTA ENLAZADA (verificado por
disasm de nf_hook_slow + helper 0xc09897d4):
- `__ip_local_out(net, sk, skb)` carga la CELDA de entries desde
**[net+0x58c]**, la almacena en state+0x1c, llama a nf_hook_slow
- recorrido: `entry = *cell`; while(entry){ if (state->[4] <= entry->[0x20])
call entry->[0xc](entry->[0x14], skb, state); entry = entry->[0]; }
— es decir, **fn@entry+0x0c, priv@entry+0x14, priority@entry+0x20,
next@entry+0x00**; state+4 = umbral INT_MIN (siempre pasa)
- init_net = **0xc1104548** (CONFIG_NET_NS=n → sock_net() inlinea la
constante; 6678 refs movw/movt, campeón del histograma; confirmado de forma cruzada por
el propio literal de nf_hook_slow). OBJETIVO: **[init_net+0x58c] = 0xc1104ad4**
- El hook LOCAL_OUT se ejecuta en el contexto del proceso EMISOR → el commit_creds(prepare_kernel_cred(0)) de nuestra hookfn da root al proceso que envió
el paquete. Trigger = sendto(127.0.0.1:9) UDP.
### Diseño del modo nf (poc/stage3.c, modo `nf 200`)
- páginas de patrón kbm (relativas a página, única fuente de verdad — el viejo
arma tenía TRES bugs ahora corregidos: proc_handler@+0x14 no +0x18; el objetivo W1
debe ser memoria de KERNEL (ctx de kworker, sin mm); código incrustado en
page+0x600 vs el desajuste entry G+0x600=page+0x8a4):
- +0x2ac/+0x2bc/+0x2c0/+0x2dc/+0x3a8: cadena falsa de phy-alloc (sin cambios)
- +0x600: hookfn nf_code (almacenamiento de marcador + prepare_kernel_cred +
commit_creds + return NF_ACCEPT(1))
- +0x700: entry falsa {next=0, fn=PM_PAGE+0x600, priv=0, prio=0x100}
- +0x740: cell → PM_PAGE+0x700
- payload: N = PM_PAGE+0x740 (0xc154b740), P = 0xc1104ad4
→ W1: *(cell+4)=P (aterriza en nuestra página), W2: *(init_net+0x58c)=cell
### Instrumentación AUTO-DIAGNOSTICABLE (kbm_scan_for)
Se conservan los mapeos de CPU de kbm; tras el free escaneamos cada página
rociada en busca de una palabra conocida:
- scan(HOOKS_PTR_ADDR) en page+0x744 → prueba reclaim + unlink + revela
qué página física respalda la conjetura de G
- scan(0x600d600d) en page+0x7f0 → prueba que la hookfn SE EJECUTÓ
(nf_code escribe este marcador como su 2ª acción)
### LA EJECUCIÓN QUE IMPORTA (2026-09-11, final de la sesión 9)```
[+] W1 CONFIRMED: region 135 page+0x185000 <- 0xc1104ad4 (reclaim + G hit!)
[+] nf trigger done, uid=2000 euid=2000
La cadena de armas completa se disparó en un arranque real: el evento reclamó el slot, el fake se ejecutó, el unlink se ejecutó, la página G-guess (0xc154b000) era genuinamente nuestra (región 135 página 389). W2 = *(0xc1104ad4)=cell es la instrucción adyacente — debe haberse ejecutado. Sin embargo, el sendto UDP no nos dio root → el fallo está DENTRO de la ruta del hook: semántica del walk, el cableado de [state+0x1c], la comparación de prioridad, o los campos de entrada. (Se añadió el experimento del marcador para discriminar hookfn-ejecutado-vs-no; solo arranques con crash antes del final de la sesión — aún sin datos limpios.)
pmap 200) es la comprobación de cordura del entorno (4/4
aciertos en caliente; fallo con crash en frío)nf 200 en arranques con retardo de asentamiento hasta que aparezca
la línea W1-CONFIRMED, luego leer la línea MARKER:
a. marcador PRESENTE, uid!=0 → fallaron las creds del shellcode (comprobar
las direcciones de prepare_kernel_cred/commit_creds; codificaciones blx)
b. marcador AUSENTE → el walk nunca nos llamó: verificar con un SEGUNDO
marcador escrito por... siguientes diagnósticos: hookfn que SOLO escribe
el marcador y devuelve 1 (sin creds) — si sigue ausente:
ls -la; dejar los archivos marcadorpoc/stage3.c: pin/root/drain{,2,3,4,5}/iso — arma completa +
oráculo + arnés de aislamiento, forense de puntos de crash con O_SYNCinit_net incorrecto: 0xc1104548 es __stack_chk_guard (el histograma
movw/movt estaba contaminado por cargas del canario de pila — 2025 construyó
todo el plan nf sobre él). El init_net real = 0xc1185040 (confirmado:
ip_send_skb(net,...) llamado con este literal; ~994 referencias todas en la
pila de red). Celda IPv4 LOCAL_OUT = init_net+0x58c = 0xc11855cc.nf_iterate trata [init_net+0x58c] como el propio
puntero nf_hook_ops — lee fn@+0xc, priv@+0x14, prio@+0x20
directamente de ese valor. La entrada fake de la sesión 9 vivía en
PM_PAGE+0x700 con la celda apuntando a ella (campos / sin usar
en la celda → → crash). La entrada fake DEBE vivir (0xc154b740). Con esto corregido, la
llamada al hook quedó probada (modo = SAFE_FN procesa los 200 sendtos
limpiamente).commit_creds(prepare_kernel_cred(0)) / override_creds(&init_cred) da
uid 0 pero aterriza en el SID de SELinux kernel, al que esta política de Fire
OS no permite escribir /data ni /sys/fs/selinux/enforce (verificado:
EACCES). El SID real de init (7) también denegado (prueba con struct cred
falso). enforcing_setup es __init (liberado → crash). El atomic_sub de
mark_reclaim necesita nents=1, lo que rompe la salida temprana de
shrink_cpu_mapping.
La victoria: el proceso del exploit conserva los mapeos de CPU de kbm, así que la entrada nf falsa puede reescribirse in situ entre paquetes:
selroot: entrada = {fn=0xc01d503c (mov r3,#0;str r3,[r0];bx lr), priv=0xc1213ea8 (selinux_state.enforcing)}.*(enforcing)=0 → SELinux Permissive.kbm_cpu[reg]+off a {fn=commit_creds, priv=&init_cred}.commit_creds(&init_cred) en la tarea del emisor → uid 0 con
un SELinux permisivo → root usable, todo en una sola reclamación, sin
necesidad de cadena.[+] W1 CONFIRMED: region 67 page+0xad000 <- 0xc11855cc (reclaim + G hit!) [*] sendto 0 -> -1 errno=1 uid=0 euid=0 [+] nf trigger done, uid=0 euid=0 <<< HOOK RAN - commit_creds OK [+] ROOT: uid=0 euid=0 [+] setenforce write=1 [+] su copied bytes=236220
- `getenforce` → **Permissive**; st3 en pausa es `Uid: 0 0 0 0`,
`CapEff: 3fffffffff`.
- `/data` está montado **nosuid**, por lo que un `su` setuid no puede funcionar. Un pequeño
`rootshell` (envía UDP → `commit_creds` sobre sí mismo → `execl sh`) proporciona una
shell root interactiva: `uid=0(root) context=u:r:kernel:s0`.
- Root puede leer/escribir `/dev/block/by-name/*` (`dd if=boot ...` OK).
### Estado del código de la Etapa 3 (`poc/stage3.c`)
- modos: `nf <mb> [probe|uprobe|oc|chain|fc <sid>|notrig|selroot]`
- `selroot` es el arma funcional. Estáticos clave: `init_net=0xc1185040`,
`HOOKS_PTR_ADDR=0xc11855cc`, `ENFORCING_ADDR=0xc1213ea8`,
`ZERO_GADGET=0xc01d503c`, `commit_creds=0xc014993c`,
`init_cred=0xc1114f54`.
- La post-explotación usa syscalls directas (sin `system()`); mantén vivo el kctx
(`pause()`) para evitar un crash durante el desmontaje.
### Pendiente (Etapa 4/5)
- Persistencia tras reinicio (verity / imagen de boot / recovery), ya que el secuestro
de celda + SELinux permisivo son solo en tiempo de ejecución y volver a ejecutar el exploit
requiere el lanzamiento de moneda de ~1/3 de recuperación.
- `su` necesitará un home no-nosuid (`/system`) o un lanzador que lo reactive.
## SESIÓN 11 — RECONOCIMIENTO DE PERSISTENCIA (Track B + Track A) y el traspaso de RE
El objetivo era root persistente. Se delimitaron dos vías:
- **Track B**: deshabilitar el arranque verificado (dm-verity / SELinux) para poder parchear `/system`.
- **Track A**: volver a ejecutar el exploit en el arranque.
Ambas se reducen al mismo bloqueo: **hacer que LK trate el dispositivo como `eng`/`unlocked`.**
### Hechos del arranque verificado (build exacta)
- Bootloader bloqueado, AVB `green`, `ro.boot.unlocked_kernel=false`, `ro.boot.secure_cpu=1`,
`rpmb_state=1`. Bootrom parcheado (sin BROM); preloader solo vía CMD short.
- `/system` es montado por **Android dm-verity desde la cmdline del kernel construida por lk**:
`root=/dev/dm-0 dm="system none ro,0 1 android-verity PARTUUID=b6404ef3-… "`,
`veritykeyid=id:f3530e18f64d11fc25eb2dd762979f078de990bf`, `androidboot.veritymode=eio`,
`skip_initramfs` (system-as-root). `dm-0` = dispositivo verity llamado `system`; `dm-1` = `/vendor`.
- LK: Amazon **UFBL**, `ro.boot.lk_version=0x0006`, build `0db73c9-20231025_030009`;
preloader `pl_version=0x000a`, build `80c6fcb-20230523_065640`. `/dev/block/by-name/lk` = mmcblk0p5 (1 MB).
- GPT completa (16 particiones, sin `persist`/`seccfg`/`nvram`/`protect`/`para`):
`proinfo` p0, `PMT` p1, `kb` p2, `dkb` p3, `lk` p4, `tee1` p5, `tee2` p6, `metadata` p7,
`MISC` p8, `reserved` p9, `boot` p10, `recovery` p11, `system` p12, `vendor` p13,
`cache` p14, `userdata` p15. eMMC boot0 (1 MB) = preloader (magic `EMMC_BOOT`);
boot1 (4 MB) = almacén IDME.
### Hallazgos estáticos de LK (UFBL)
Cabecera de `lk.img`: `88 16 88 58 | 00052974 | "LK"`; tabla de vectores ARM en 0x200, el resto Thumb-2,
independiente de posición/reubicado (los literal pools usan `ldr+add pc`, por lo que un desensamblado ingenuo relativo a la base falla).
Cadenas relevantes (offsets de archivo): `amzn_image_verify`, `amzn_verify_unlock`, `amzn_verify_code_internal`,
`unlock_code`, `unlock code error`, `unlock failed`, `$Common Kernel Signing Engineering CA0`,
`seccfg`, `para`, `ENV_v1`, `LK_ENV`, `Kfos_flags`/`Kdev_flags`/`Kusr_flags`/`Kunlock_code`/`Kunlock_version`,
`FOS_FLAGS_{NONE,ADB_ON,ADB_ROOT,CONSOLE_ON,RAMDUMP_ON,VERBOSITY_ON,ADB_AUTH_DISABLE,FORCE_DM_VERITY,DM_VERITY_OFF,BOOT_DEXOPT}`,
`[DM-VERITY] verify for system(root) is enabled`, `[DM-VERITY] verify off by fos_flags`,
`[DM-VERITY] disabled by fos_flags on eng devices or unlocked device`,
`[SELINUX] set to permissive mode by dev_flags`, `androidboot.prod=1|0`, `androidboot.unlocked_kernel=%s`.
**Conclusión: LK condiciona los efectos de seguridad de `fos_flags`/`dev_flags` a eng/unlocked.**
### Almacén IDME (eMMC **boot1**) — escribible, persistente, leído por LK y Android
- Magic `beefdeed` + `"2.1\0"` + count(0x19=25) en 0x0; elementos desde 0x10.
- Formato de elemento: `char name[16]; u32 size; u32 type(=1); u32 magic(=0x124); u8 data[size] (pad4)`.
- Offsets de elementos (prístinos): `board_id@0x10 serial@0x3c mac_addr@0x68 mac_sec@0x94 bt_mac_addr@0xd0
bt_mfg@0xfc product_name@0x198 productid@0x1d4 productid2@0x210 region@0x24c bootmode@0x26c
postmode@0x28c bootcount@0x2ac manufacturing@0x2d0 unlock_code@0x4ec sensorcal@0x908 alscal@0x9c4
KB@0xa00 DKB@0x1e1c device_type_id@0x2238 dev_flags@0x2274 fos_flags@0x2298 usr_flags@0x22bc
wifi_mfg@0x22e0 unlock_version@0x26fc`. Los valores son ASCII (los flags son **cadenas hex**).
- Lectura en tiempo de ejecución: `/proc/idme/<name>` (solo lectura). Los valores del último arranque se cachean; una escritura en boot1 surte
efecto en el siguiente arranque. La ruta de escritura requiere limpiar `/sys/block/mmcblk0boot1/force_ro` (root).
- **Confirmado que LK lee boot1**: cambiar `serial` cambió `ro.boot.serialno` en el siguiente arranque.
Pero LK **trunca serial a 16 bytes** e ignoró `fos_flags=0x80`, `dev_flags=0xff`,
all-ones, etc. — verity/selinux/`prod` sin cambios. Así que la inyección en cmdline vía serial falla.
### Consumidores en el lado Android de los flags IDME
- `/init.fosflags.sh` (servicio `fosflags`, `u:r:fosflags:s0`): `FOS_FLAGS_ADB_ON=0x1`,
`CONSOLE_ON=0x4`, `RAMDUMP_ON=0x8`, `VERBOSITY_ON=0x10`, `ADB_AUTH_DISABLE=0x20`,
`BOOT_DEXOPT=0x100`. Verificado: establecer flags surte efecto (`sys.usb=adb`, `noadbauth=1`).
- **adbd** (ARM ET_EXEC sin strip; `.text` VA 0x8160 / archivo 0x160; fileoff = VA-0x8000):
- `amzn_is_root_allowed` @0x2d5b8 = `amzn_is_dev_unlocked() && (fos_flags & 0x2)`
- `amzn_is_adb_auth_disable_allowed` @0x2d5e8 = `fos_flags & 0x20` (sin condicionar)
- `amzn_is_dev_unlocked` @0x2d5fc = `/proc/cmdline` contiene `androidboot.prod=0` **o**
`androidboot.unlocked_kernel=true`
- `fos_read_debug_flags` @0x2d724 lee `/proc/idme/<name>` y parsea **hex**
- `restart_root_service` @0xcb74 / `restart_unroot_service` @0xcc64
- cadenas: `amzn_fos: ADB: Auto-root succeeded`, `… eng_device=%d`, `… unlocked_kernel=%d`,
`adbd cannot run as root in production builds`, `ro.debuggable`
- `adb root` → "cannot run as root in production builds" (`ro.debuggable=0`) — así que incluso con la
puerta de auto-root satisfecha, la comprobación de prod de AOSP condiciona la ruta del comando.
### Por qué el root de la Sesión 10 no persiste
- SELinux permisivo + secuestro de celda + root son solo en tiempo de ejecución.
- `/data/metrics` es una **vpartition**: `/system/bin/vpartition.sh` monta `/data/vp/metrics.img`
(ext4, non-nosuid/noexec) en `/data/metrics` en cada arranque; un `su` escrito ahí **no**
sobrevive al reinicio. (También es la razón por la que un `su` setuid daba uid 0 pero **cero caps**.)
### Track A (re-explotación en el arranque) — bloqueado
- Ningún trigger `.rc` de init ejecuta código controlable (imports todos verificados; los triggers `persist.*` solo
`start` servicios fijos; scripts en `/system`/`/vendor`).
- Los servicios root leen configuraciones de `/data` pero nunca ejecutan desde ellas (`perfmonitord`, `amazonfiled`,
`vpartition.sh`, `kisd`, …).
- El único ejecutor en el arranque = una **app**, pero la huella en pausa del exploit es **VmRSS 534 MB**
(spray de `kbm`) → lmkd la mata; además una recuperación perdida provoca un pánico (`PANIC_ON_OOPS`) → bootloop.
- **adbd auto-root** existe pero está condicionado por la cmdline construida por LK (`prod=0`/`unlocked_kernel=true`).
### Conclusión / siguiente objetivo (elegido: RE de Track B)
Todo depende de hacer que LK reporte `eng`/`unlocked`. A nuestro alcance:
`androidboot.prod=1|0` y `androidboot.unlocked_kernel=false` son establecidos por LK. Reversear LK para encontrar:
1. dónde lee `fos_flags`/`dev_flags`/`usr_flags` (los elementos `K*`) y la puerta exacta;
2. la determinación de `prod`/`unlocked` (¿elemento IDME? ¿buildvariant? ¿resultado de `amzn_verify_unlock`?);
3. `amzn_verify_unlock` (verificación RSA de libtomcrypt) para un bypass o una ruta débil de unlock_code/version;
4. el almacenamiento `seccfg`/`para`/`ENV_v1`(LK_ENV) (no está en ninguna partición volcada — quizá protegido por tee);
5. el preloader (`boot0`, `EMMC_BOOT`) en busca de un bug.
Si cualquiera de estos nos permite establecer eng/unlocked (de forma persistente, vía boot1 o una escritura directa en partición), entonces
`FOS_FLAGS_DM_VERITY_OFF` deshabilita la verity de system(root) y `/system` puede parchearse de forma persistente.
### Artefactos (de esta sesión)
`/tmp/opencode/mustang-dumps/` (puede borrarse al reiniciar el host): `lk.img`, `boot1.img` (prístino),
`boot.img`, `MISC.img`, `metadata*.img`, `pmt.img`, `mbr.img`, `kb.img`, `dkb.img`, `reserved.img`,
`cache.img`, `boot0.img`, `boot1.img`, `adbd.bin`, `perfmonitord.bin`, `amazonfiled.bin`.
Helpers: `tools/findinitnet.py`, `findgadget*.py`, `findstores.py`, `adbd_sym.py` (en /tmp);
el repo tiene `run.sh`, `poc/stage3.c` (`selroot`), `poc/su.c`, `rootcmd.sh`.
### Comandos útiles```
# IDME read
/data/metrics/su sh -p -c 'for f in fos_flags dev_flags usr_flags serial region device_type_id unlock_version; do echo -n "$f="; cat /proc/idme/$f; echo; done'
# write boot1 (root; su lives only until reboot -> re-run run.sh first)
/data/metrics/su sh -p -c 'echo 0 > /sys/block/mmcblk0boot1/force_ro; dd if=/data/local/tmp/boot1.img of=/dev/block/mmcblk0boot1 bs=4096 count=4; sync; echo 1 > /sys/block/mmcblk0boot1/force_ro'
# dump a partition to host
adb exec-out '/data/metrics/su dd if=/dev/block/by-name/lk bs=4096 2>/dev/null' > lk.img
Objetivo: conseguir que LK trate el dispositivo como eng/unlocked, o encontrar un bug en el preloader/LK,
para que verity/SELinux puedan desactivarse de forma persistente. Resultado: se invirtió la ruta de código relevante
de LK de extremo a extremo; el cambio no es alcanzable mediante los almacenes disponibles.
Ningún dispositivo fue brickeado; el único experimento con boot1 fue revertido a su estado original.
lk.img comienza con un pequeño stub ARM (archivo 0x200). El reubicador en 0x224
copia desde 0x200 a un destino literal y salta a una entrada literal:
Así que dirección en tiempo de ejecución = 0xFF400000 + offset de archivo para offsets >= 0x200.
Todo lo que sigue al stub es Thumb-2, independiente de posición. Las cadenas se construyen
con ldr rT,[pc,#imm] (offset T1 = imm8*4; offset de ldr.w = imm12) seguido de
add rT, pc; el objetivo es (add+4) + *pool. Se añadió un escáner robusto que sobrevive
al stub ARM y a los pools de literales como tools/lk_xref.py (maneja las formas de 16 y
32 bits, escanea cada 2 bytes). Todos los offsets a continuación son offsets de archivo;
súmese 0xFF400000 para las direcciones en tiempo de ejecución.
0x20b4 lee el ítem IDME unlock_code (0x400 bytes, todo ceros en esta
unidad) y ejecuta amzn_verify_unlock (0x222c -> 0x20f0). Esa función acciona
libtomcrypt (docenas de rutas /features/libtomcrypt/src/pk/asn1/der/... y
verificación RSA), y la imagen incorpora el material del certificado:
Sunnyvale / Amazon Lab126 / "$Common Kernel Signing Engineering CA0" en
0x317d9+, además de los diagnósticos
Image FAILED AUTHENTICATION on PRODUCTION device (0x3166e),
Authentication failed on engineering device with production certificate (0x316a0),
Image FAILED AUTHENTICATION on ENGINEERING device (0x31703),
Image AUTHENTICATED with PRODUCTION certificate (0x31736).
No hay atajo de código vacío / longitud / versión: verify(zeros) != 0, por lo tanto
(confirmado en ). Cambiar
o requiere o bien un
válido firmado por Amazon (clave privada no disponible) o un bug de ejecución de código en
el verificador. No se encontró nada explotable (bounds/size) estáticamente en
0x20b4/0x222c/0x20f0. =>
Los flags de seguridad se leen a través del getter en 0x57c. Prueba empírica:```
dd if=/dev/block/mmcblk0boot1 ... ; reboot /proc/idme/fos_flags -> 00000080 (persisted, Android sees it) ro.boot.veritymode -> eio (unchanged!) root=/dev/dm-0 dm="system none ro,0 1 android-verity PARTUUID=..." (unchanged) androidboot.prod=1 / secure_cpu=1 / buildvariant=user (unchanged)
`fos_flags=0x80` es `FOS_FLAGS_DM_VERITY_OFF`; la puerta decodificada habría desactivado
verity **si** el getter lo hubiera devuelto. No lo hizo. La puerta está activa, no es
código muerto: su centinela de caché de una sola vez es `-1` en la imagen
(`*(u32*)0x50c74 == 0xffffffff`), por lo que la función realmente ejecutó la
ruta `check_flag("fos_flags",0x80)` y obtuvo 0. Por lo tanto, el getter (al menos
en el momento de la protección de verity) **no** está leyendo los elementos IDME de boot1.
El otro almacén candidato es el **LK env**, cargado desde una partición literalmente
llamada `"para"` (loader 0x12fd4, magic `ENV_v1`, checksum @0x3ffc). La propia tabla
de particiones de LK (0x4fcc0..0x50340) lista preloader/proinfo/nvram/protect1/
protect2/persist/seccfg/secro/**para**/logo/custom/expdb/tee1/tee2/metadata/
system/cache/userdata — pero el GPT real de la tablet tiene **solo 16 entradas**, todas
de tipo `af3dc60f838472478e793d69d8477de4`:```
#0 proinfo 0x400 #1 PMT 0x1c00 #2 kb 0x4000 #3 dkb 0x4800
#4 lk 0x5000 #5 tee1 0x5800 #6 tee2 0x8000 #7 metadata 0xa800
#8 MISC 0x1e400 #9 reserved 0x1e800 #10 boot 0x22800 #11 recovery 0x2a800
#12 system 0x34800 #13 vendor 0x644000 #14 cache 0x6b4800 #15 userdata 0x7ae800
No hay partición para, seccfg, nvram, protect ni persist en
este producto (y los volcados de PMT/pmt.img son todo ceros). Así que el entorno de LK está vacío,
las claves Kfos_flags/Kdev_flags nunca existen, y todas las comprobaciones de fos_flags/dev_flags
se resuelven a 0 — independientemente de lo que contengan los elementos IDME. Los elementos IDME de boot1
son consumidos por Android (/init.fosflags.sh, adbd,
/proc/idme/*) pero no por las puertas de seguridad de LK.
unlocked_kernel requiere un unlock_code firmado por Amazon (RSA/libtomcrypt,
CA embebida). No es falsificable offline; no se encontró ningún bug en el verificador. Bloqueo duro.DM_VERITY_OFF / selinux=permissive se consumen del entorno de LK
(para/ENV_v1), que no existe en este GPT. IDME fos_flags es
empíricamente ignorado por LK (0x80 persistió, verity se mantuvo en eio). Bloqueo
duro a menos que se modifique la tabla de particiones.fos_flags=0x80 exitoso solo establecería androidboot.veritymode= disabled y un root= distinto de dm-0; no desbloquearía, y SELinux
seguiría necesitando dev_flags del mismo entorno ausente para pasar a permissive.para/ENV_v1: añadir una entrada GPT llamada para (GPT primario
userdata
(userdata termina en LBA 0x3a3dfde; disco = 30535680 sectores), luego fabricar un entorno con
fos_flags=0x80 y dev_flags=0x40 (checksum en +0x3ffc = suma de bytes sobre
0x3ffc). Esta es la única ruta restante para desactivar verity. Riesgos: corromper
el GPT primario/de respaldo puede brickear; y no se demostró que la puerta de verity
realmente lea para (solo que no es boot1 IDME).boot0/EMMC_BOOT): no revertido en esta sesión. Escribir
boot0 está prohibido hasta que exista una copia prístina y una ruta de recuperación.tools/lk_xref.py — resolvedor de xref de cadenas de LK independiente de la base./tmp/opencode/mustang-dumps/lk.img, boot1.img (prístino),
boot0.img, mbr.img (GPT), pmt.img (todo ceros)./tmp/opencode/s12/boot1_f80.img; dispositivo restaurado a boot1 prístino
(verificado /proc/idme/fos_flags -> 0)../run.sh)```./run.sh --no-build
/data/metrics/su /system/bin/sh -p -c 'cat /proc/cmdline'
for f in fos_flags dev_flags usr_flags unlock_version serial; do cat /proc/idme/$f; echo; done
## SESIÓN 13 — el preloader es accesible después de todo (`1949:20ff` = preloader MTK, transporte HID)
Mientras está apagada y conectada por USB, la tablet se enumera como **`1949:20ff`**
(`Lab126`) — *no* Android y *no* el bootrom `0e8d:0003`. Descriptor:```
bInterfaceClass 3 (HID), iConfiguration "HID", iInterface "HID Interface"
HID report descriptor = 05 01 09 00 a1 01 c0 (empty collection!)
EP 0x81 IN interrupt 4 bytes, bInterval 4
EP 0x01 OUT interrupt 4 bytes, bInterval 4
iSerial = GCC0X90805310009 (the IDME serial)
Identificación. 0x20FF aparece como "MTK Preloader" en el archivo
config/usb_ids.py de mtkclient (bajo el VID de MediaTek 0x0e8d: 0xe8d:{0x0003 Brom, 0x2000/0x2001/0x20ff/0x3000 Preloader}). Amazon mantuvo el PID del preloader y
cambió el VID a 0x1949, y lo presenta como un par de endpoints HID con un descriptor
de reporte ficticio. Por lo tanto, este es el modo preloader / USBDL de MediaTek, una etapa
por debajo de LK — alcanzada aquí mediante apagado + conexión, no mediante el cortocircuito de CMD.
Las cadenas del descriptor "HID"/"HID Interface" no están presentes en lk.img,
boot0.img, boot.img ni en los demás volcados, es decir, el modo es producido por un
componente que no hemos volcado (bootrom/TEE) o se ensambla en tiempo de ejecución.
Por qué esto importa. El preloader de Amazon utilizado por aftv2-tools expone
comandos integrados, sin Download-Agent, a través de este flujo de bytes exacto:```
handshake : host A0 0A 50 05 -> dev 5F F5 AF FA
0xD1 read32 (addr, n_words) : echo cmd/addr/n, 00 00, nu32, 00 00
0xD4 write32(addr, words[]) : echo cmd/addr/n, 00 00, nu32, 00 00
`aftv2-tools/read_mmc.py` usa `read32`/`write32` para manipular el controlador MSDC
(base `0x11230000` en MT8173; verificar para MT8163) y leer/escribir **bloques eMMC
en bruto** sin DA y por lo tanto sin AVB/verity de por medio. Si el preloader de mustang
acepta 0xD1/0xD4, esa es una vía directa al desbloqueo persistente (parchear `boot` /
`lk`), independiente del código de desbloqueo RSA y del ausente entorno LK.
### Herramientas añadidas (se requiere root; hacer chmod al nodo USB primero)```
lsusb -d 1949:20ff # note Bus/Dev, e.g. Bus 001 Device 003
sudo chmod 666 /dev/bus/usb/001/003
# 1) does it answer the MTK handshake? (no DA, no flash access)
nix-shell -p python3Packages.pyusb --run \
'python3 tools/mtk_preloader_hid.py handshake'
# 2) read-only arbitrary memory read
nix-shell -p python3Packages.pyusb --run \
'python3 tools/mtk_preloader_hid.py read32 0x00100000 4'
tools/probe_preloader.py es la sonda mínima de solo handshake;
tools/mtk_preloader_hid.py es el transporte completo (handshake/read32/
write32; write32 está protegido y no debe usarse hasta que el mapa de
registros de la eMMC esté confirmado).
read_mmc), luego parchear boot.img/lk desde el preloader y reiniciar.amzn_verify_unlock ni de la puerta de entorno de LK.pi_blocked_onfutex.c:1975 pasa this->rt_waiter,
declarado en futex_wait_requeue_pi en futex.c:2880) → el waiter marca su propio frame liberado
mediante select de arm32 (nr 142) fd_setsDEBUG_RT_MUTEXES desactivado
→ rt_mutex_waiter compacto de 48 bytes (tree_entry@0, pi_tree_entry@0xc, task@0x18,
lock@0x1c, prio@0x20, deadline@0x28)modprobe_path @ 0xc111488c (cadena auto-localizada en
vmlinux; KALLSYMS_ALL desactivado, así que los símbolos de datos necesitan este truco) → exec de binfmt desconocido → script
root (setenforce 0, desactivar OTA, su)refs/: NebuSec/CyberMeowfia (original), GhostLock-5.10 (port a Fire OS 8,
trigger completo de ARM de 32 bits en src/exp32/), ghostlock-...-4.19-k40 (port a Android Qualcomm 4.19)selrootselinux_state.enforcingcommit_creds(&init_cred)uid=0jcnr_extresinfo->gpu_alloc_addr (una GPU VA que debes
pre-asignar y pasar)| objeto expulsable | presión | resultado |
|---|
| ninguno | 900 MB | sobrevivió |
| ninguno | 1300 MB | panic (bug de lowmem del sistema — no relacionado) |
| región normal + DONT_NEED | 700 MB | sobrevivió |
| región JIT + DONT_NEED | 700 MB | panic en la ruta de reclaim |
kernel/config-* — volcado de /proc/config.gz del dispositivo en ejecuciónksrc/ — código fuente OSS de Amazon (platform.tar + árbol midgard-r26p0 extraído)/tmp/opencode/mustang_ota.bin (sha256 6068515a… coincide con fireos-archive)
y tarball de código fuente del kernel de 2.2 GB conservado en ~/Desktop/amazon-mustang/ksrc/kernel/mediatek/mt8163/4.9/drivers/misc/mediatek/gpu/gpu_mali/mali_midgard/midgard-r26p0/| sprayers de rename concurrentes durante la tormenta | los renames se estancan (journal/GFP_NOFS) → 128 total → desreferencia basura |
| pre-drenar 12K eventos + presión + pequeño final | crash en la desreferencia |
| + kill-child-en-expulsión (poll de 10ms) | crash en la desreferencia |
| + ciclo de vida fijado a cpu0 (drain3) | crash en la desreferencia |
| presión escalonada (drain4 v1) | los hijos liberaron memoria al salir → sin expulsión (ruta legal validada) |
| ejecución | resultado |
|---|
| pmap G=c2a412a4 400MB | crash en deref |
| pmap G=c2f4b2a4 480MB | crash; +0 renombrados post-kill (480MB asfixia el fs) |
| iso2 (spray + regiones + oráculo) | ACIERTO DE REGIÓN — el spray no rompe la reclamación |
| mix v1 (eventos+regiones concurrentes) | crash; confundido (hilos de eventos en spin) |
| pmap G=c2a7d2a4 350MB + retouch | crash; +4452 renombrados OK |
| mix2 (secuencial: 2s eventos LUEGO regiones) | crash; +7126 renombrados (28K allocs de eventos), 2715 regiones |
nextfnfn=0PM_PAGE+NF_CELL_OFFprobekernel_x_end:
arch/arm/mm/mmu.c map_lowmem() mapea lowram por debajo del texto del
kernel como MT_MEMORY_RWX, pero todo lo que está por encima de
kernel_x_end como MT_MEMORY_RW → PMD_SECT_XN (línea 509). El shellcode
incrustado en 0xc154b600 provoca prefetch-abort. El payload debe ser un
puntero a función real del kernel, no código en el physmap.| literal (offset de archivo) | valor | significado |
|---|
| 0x270 | 0xFF4002F8 | scratch de str r4,[r6] |
| 0x274 | 0xFF40027C | destino (base+0x27C) |
| 0x278 | 0xFF54A440 | fin de copia (incl. BSS) |
| 0x27C | 0xFF400484 | punto de entrada |
| offset | función |
|---|
0xdf7c | is_secure_or_prod() -> byte[ [[g]+0 ] + 0x163 ]; g = global @0x52838. 1 en esta unidad. |
0x20b4 | verify_stored_unlock() = memset(buf,0,0x100); lee IDME/env unlock_code (0x100) vía 0x57c; bl 0x222c; devuelve (verify==0). |
0x222c / 0x20f0 | amzn_verify_unlock(code,len) — verificación RSA/PKCS#1 de libtomcrypt (ver abajo). |
0xda3e | is_unlocked() = is_secure_or_prod() && verify_stored_unlock(). |
0x29a28 | is_verity_disabled() = (fos_flags & 0x80) && !(is_secure_or_prod() && verify_stored_unlock()); cacheado en el global @0x50c74. |
0x29974 | Constructor de cmdline de SELinux: dev_flags & 0x20 -> androidboot.selinux=enforce, dev_flags & 0x40 -> ...=permissive (cada uno condicionado por byte[+0x162]). |
0x118xx/0x11bxx | Constructor de cmdline del kernel (unlocked_kernel, prod=1/0, verifiedbootstate, rpmb_state, secure_cpu, versiones, root=). |
0x27af8 | Puerta UART: fos_flags & 0x4 -> printk.disable_uart=0, si no =1. |
0x12fd4 | Cargador de env de LK: partición "para", 0x4000 bytes, magia ENV_v1, checksum = suma de bytes sobre 0x3ffc comparada con la palabra @0x3ffc. |
0x1efd0 | búsqueda de partición por nombre (usado para "para", "boot", ...). |
0x57c | despachador getter a través del slot de callback @0x58218; los slots @0x58200..0x5821c se registran desde una tabla en 0x5a8-0x734. |
0x2a19c | fastboot oem unlock: bl 0x222c(code,len); en caso de éxito escribe unlock_code (0x100) vía 0x408. |
unlocked_kernel=false/proc/cmdlineandroidboot.unlocked_kernelandroidboot.produnlock_code