
Pesquisa de exploit de kernel que obtém root temporário no Amazon Fire 7 (Fire OS 7.3.3.1) através do use-after-free do JIT do Mali kbase CVE-2022-38181, com uma cadeia de sobrescrita de modprobe_path.
Projeto assistido por IA. Esta pesquisa, desenvolvimento de exploit e documentação foram produzidos com assistência de IA usando os modelos GLM-5.3 e DeepSeek V4.1 Flash.
Pesquisa de exploit de root para o Amazon Fire 7 9ª geração (mustang, MT8163, Mali-T720) no firmware final — Fire OS 7.3.3.1, PS7331.4463N, kernel 4.9.117 (compilado em 2025-05-03, SPL 2024-08-01).
Objetivo: LineageOS. O caminho do bootloader está morto nesta unidade (bootrom corrigida — apenas preloader via curto de CMD), então a única rota restante é um exploit de kernel por software.
nix-shell -p android-tools --run './run.sh' # add -p zig too if no zig
Em caso de sucesso:```
/data/metrics/su id # run a command as root
/data/metrics/su # interactive root shell
O reclaim ganha aproximadamente 1 boot em 3 e uma perda entra em pânico/reinicia o tablet;
run.sh apenas aguarda o reboot e tenta novamente. O SELinux é forçado para Permissive como
parte do exploit, então o root é apenas em tempo de execução — um reboot restaura o estado
original e você executa run.sh novamente.
Os binários pré-compilados st3 e su (armv7 estático) estão commitados, então nenhum toolchain é
necessário para executar. ./run.sh --build os recompila a partir de poc/*.c se você tiver zig.
O GhostLock (abaixo) está arquivado: a variante BUG_ON rtmutex da MTK + nenhuma divulgação de endereço de kernel a partir do shell = beco sem saída arquitetural nesta build (sessões 2-4). O UAF do JIT do kbase foi rediagnosticado (o "pânico incondicional" do destroy-worker era o deref do JIT_FREE, log perdido pela morte do adbd no meio do pânico) e o estágio 2 agora está provado por oráculo — veja a seção SESSÃO 5.
UAF de pilha do futex-PI em remove_waiter() do rtmutex (divulgação NebuSec 2026-07, correção 3bfdc63936dd
aplicada em 2026-04). Faixa vulnerável 2.6.39–7.1 → nosso 4.9.117 (maio de 2025) é afetado.
Verificado na nossa build exata:
CONFIG_FUTEX=y, rtmutex compilado, bug presente literalmente:
rtmutex.c:1108-1111 usa current->pi_lock/current->pi_blocked_on (deveria ser
waiter->task); local de chamada com bug rtmutex.c:1723 (caminho de erro de rt_mutex_start_proxy_lock)WAIT_REQUEUE_PI/CMP_REQUEUE_PI), sem nó de dispositivo,
nada controlado pelo SELinux — os obstáculos fatais do caminho kbase não existem aquisched_setattr → __sched_setscheduler → rt_mutex_adjust_pi(p) em
sched/core.c:4706 — desreferencia obsoleto ✓exp32/main.crt_waiter vs área fd_set de do_sys_select — desmontar
nosso vmlinux (do_sys_select stack_fds vs frame de futex_wait_requeue_pi), expor
STAMP_NFDS/STAMP_WAITER_OFF como ajustáveisFailed 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 em Sat May 3 01:25:15 UTC 2025 (Linaro GCC 6.3-2017.05)/dev/kb, /dev/dkb (partições de backup de kernel da Amazon) root:drmrpc 0660 — bloqueadosmali_kbase r26p0-01rel0 (Midgard, Mali-T720), dentro da faixa afetada do NVD r4p0–r31p0mali_kbase_mem.c:2721 kbase_jit_destroy_worker libera a região, nunca limpa kctx->jit_alloc[id]mali_kbase_softjobs.c:1270 kbase_jit_free_finish desreferencia o jit_alloc[ids[j]] obsoletomali_kbase_mem.c:3138 kbase_jit_backing_lost → caminho de destroy (dispara durante o reclaim)0xc0008000 / PA 0x40080000)ARM_SW_DOMAIN_PAN → ret2usr viável; CONFIG_PANIC_ON_OOPS=y (tentativas falhas = reboot)SLAB_FREELIST_RANDOM/HARDENED, sem CONFIG_USER_NS/USERFAULTFD/NF_TABLESCONFIG_MODULES=y, sem STATIC_USERMODEHELPER → sobrescrita de modprobe_path = root_IOC_TYPE 0x80)MEM_ALLOC tem 32 bytes (in tem 4 × u64 incl. extent)BASE_MEM_PROT_GPU_RD|WR (bits 2|3), não o legado R|Wmmap(fd, offset=3<<12, PROT_NONE)JOB_SUBMIT deve ser igual a sizeof(base_jd_atom_v2) = 48 (base_jd_prio/base_jd_dep_type são typedefs u8)MEM_JIT_INIT (nr 14, struct v2), alloc/free são soft jobs via JOB_SUBMIT
(BASE_JD_REQ_SOFT_JIT_ALLOC=0x209, ...FREE=0x20a; =ptr de usuário, =contagem)O pânico ocorre durante a própria eviction (evictable_reclaim_scan_objects → backing_lost →
destroy worker) — as referências pendentes (jit_alloc[], lista de evict) são percorridas antes de
submetermos JIT_FREE. O estágio 2 deve vencer a corrida: realocar o kbase_va_region liberado com nosso
próprio spray de MEM_ALLOC enquanto a pressão ainda está em execução.
poc/stage2.c — exploit do estágio 2 (modos: step/uaf/spstep/spfree/spray/keys)
— spray 700 = execução completa do oráculo; sobrevive e pausa (mate para limpar)poc/mustang_jit_uaf.c — PoC do estágio 1 (modos: jit N / control N / pressure N)poc/build.sh — cross-build com zig (musl armv7 estático)kernel/vmlinux — símbolos recuperados da build exata da 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
## Referências
- Aviso GHSL-2022-054: https://securitylab.github.com/advisories/GHSL-2022-054_Arm_Mali/
- Writeup do exploit do Pixel 6 do Mo: https://github.blog/2023-01-23-pwning-the-all-google-phone-with-a-non-google-bug/
- Precedente do Fire HD 10 (trona), mesma família de bug: ericpardee.github.io/fire-hd-ownership
- Portal OSS da Amazon: amazon.com gp/help/customer/display.html nodeId=200203720
- Tópico de desbloqueio no XDA (morto para esta rev de hw): xdaforums.com/t/fire-7-2019-mustang-unbrick-downgrade-unlock-root.3944365/
## Adendo da Sessão 3 (levantamento profundo de carimbos de syscall)
Profundidades de cópia-fonte medidas (absoluto vs syscall-entry sp0; span do waiter -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 (MELHOR — 0x1c aquém de waiter+0x00)**
- poll entries @ -0x3e0 (inteiramente abaixo; lado errado)
Descartado nesta sessão:
- cadeia io_submit rasa demais (~-0x130) | semtimedop: CONFIG_SYSVIPC=n (stub)
- configfs montado mas ZERO subsistemas registrados (sem alvos de mkdir)
- /sys/kernel/debug, /config: negados pelo SELinux para o shell
- /proc/sys/kernel: getdents funciona (29 entradas listadas), apenas pid_max ABRÍVEL;
leituras de kptr_restrict/hotplug/hostname/domainname todas negadas
- O valor de escrita é SEMPRE waiter+0 (endereço de pilha do kernel, executável, shellcode em
+0x1c): rb_link_node *link = node, insert_color escreve parent-color — nenhuma
variante de valor controlado é possível sem carimbar campos da árvore (lacuna -0x1d8..-0x1bc)
- Campos de despacho de duplo-deref leem *(waiter+0)=1, *(waiter+4/+8)=0 — BLX 1/0
(nf_hooks, net_families, inet[6]_protos, seq_file->op todos mortos)
- timer_list.function@+0xc e work_struct.func@+0xc leriam *(waiter+0xc) =
pi_tree self-ptr = waiter+0xc EXECUTÁVEL — mas nenhum caminho enfileira waiter+0 como
timer/work (corrupção de link / sem fontes de enfileiramento indireto)
- Tree-root-nonzero (slot de handler sysctl) = pointer-chase determinístico através
de .text como rb-tree; termina em uma palavra zero — simulável offline, mas aterrissar
em um slot gravável útil é implausível
Pistas restantes para a sessão 4:
1. caminhos profundos de ioctl: cópia de ifreq do dev_ioctl (40B de dados do usuário) — medir
profundidade da cadeia SyS_ioctl→sock_ioctl→dev_ioctl vs -0x1d8
2. Qualquer outra cópia 0x1c mais profunda que sendmmsg (nada encontrado ainda)
3. Se a caça à superfície de carimbo falhar: reconsiderar construções encadeadas por walk ou
caçar classes de slots graváveis-zero-chamados ainda não enumeradas
## SESSÃO 4 — OS DOIS AVANÇOS
### 1. O rtmutex_common.h da MTK é todo o mistério
A MTK substituiu o rt_mutex_top_waiter NULL-safe do 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
SEM VERIFICAÇÃO DE NULL + BUG_ON. Toda âncora totalmente zerada morre em *(NULL+0x1c); âncoras de lixo morrem no udf. A CAMINHADA EXIGE: lock->waiters_leftmost (lock+8) deve apontar para um waiter falso W (gravável) com W->lock (+0x1c) == lock.
Fluxo da caminhada totalmente mapeado (rt_mutex_adjust_prio_chain @ 0xc0189a58):
/proc/self/task//stat campo 28 (kstkesp) retorna SP REAL do kernel para threads bloqueadas em syscall a partir do contexto SHELL (verificado: valores não-zero observados).
A caminhada dispara deterministicamente (sonda de dead-lock: crash toda vez, código limpo). A escrita não pode cair por causa de uma coincidência tripla de hardening do kernel:
A janela de stamp (waiter+0x1c..0x5b) é a única memória controlada+de-conteúdo-conhecido, mas seu ENDEREÇO é o desconhecido que precisamos. Construções autorreferenciais todas exigem estampar um endereço de kernel como constante — circular sem um vazamento.
O kernel 5.4 deles tem rtmutex_top_waiter UPSTREAM (NULL-safe: if (!leftmost) return NULL) — âncoras de árvore vazia sobrevivem, a escrita deles caiu em null_fops
(graVável no kernel deles). Mesmo ELES estão presos no dispatch ("ioctl reboot").
A árvore MTK 4.9.117 do Mustang tem a variante BUG_ON — tablets Fire OS 8
(GhostLock-5.10) tiveram sucesso porque seus kernels 5.10 são estilo upstream.
Testado e morto a partir do domínio shell:
CONCLUSÃO: GhostLock no mustang exige uma divulgação de endereço de kernel que este kernel não expõe ao domínio shell. O lock falso autorreferencial não pode ser construído sem ela.
(a) Moagem boot-determinística: reboot → calibrar endereço de stack via oráculo de crash (~20-30 reboots), verificar reprodutibilidade. Long shot — alocação de stack de thread em boot tardio provavelmente instável. (b) PIVOTAR de volta para kbase CVE-2022-38181 com ativos acumulados: vmlinux de build exato + fonte completa + toolchain + disciplina de trace O_SYNC + conhecimento profundo de 4.9. O bloqueador original (panic do destroy-worker durante evicção JIT) é um problema de timing de spray, agora melhor compreendido. (c) Parar em honestos ~45%: trigger provado, caminhada mapeada até a instrução, escrita bloqueada por BUG_ON MTK + sem-vazamento.
Recomendado: (b) — o bug de kbase está verificado-presente nesta fonte exata, tinha um trigger funcional, e seu bloqueador é mecânico, não arquitetural.
Modo step (alloc id=1 → DONT_NEED → pressão de 700MB → MEM_QUERY, SEM free)
sobrevive: query=-1 (região liberada pelo destroy worker, rbtree-limpa).
O caminho do worker é byte-idêntico ao fluxo legal JIT_FREE-sob-pressão.
O crash da sessão-1 era sempre o deref pendente do JIT_FREE; sua linha de log foi
perdida porque o panic mata o adbd no meio do flush. Verificado mais duas vezes com modo
uaf (free puro → panic, mesmo corte de log). O fluxo GHSL-2022-054 está totalmente
vivo neste build.
kbase_jit_free(kctx, reg) @ 0xc058495c com reg falso totalmente controlado:
reg->cpu_alloc NULL → tamanho backed 0 → bloco de trim pulado (0xc0584978)kctx+0x147dd (byte) + kctx+0x147de+bin_id (byte)mark_reclaim(reg->gpu_alloc) @ 0xc059b158: chain
K=*(gpu_alloc+0x38) → *(K+0x1429c)==0 pula mm-atomics →
atomic_sub nents@K+0x141c8, D=*(K+4) → atomic_sub nents@D+0x538.
Com nents=0 todas as escritas são stores no-op (strex do mesmo valor).reg->flags |= 0x100000 (escrita no falso, benigna)list_add(gpu_alloc->evict_node, &kctx->evict_list): cabeça de evict_list
@ kctx+0x1427c; escreve em gpu_alloc+0x18/0x1c (deve ser gravável)r3=*(reg+0x3c) prev, r2=*(reg+0x38) next
→ *(next+4)=prev; *(prev+0)=next — dois write-what-where arbitrários,
depois relink de reg+0x38 em jit_pool_head @ kctx+0x148e89 candidatos; S=0xc118b7ec (dados xfrm, dormente neste dispositivo):
*(S+8)=0 (nents), *(S+0x18)=S+0x18 (evict_node vazio → sem WARN),
K=*(S+0x38)=0xc118b820 → *(K+0x1429c)=0, K/D+0x141c8/+0x538 todos em
dados graváveis. Alvos do oráculo preparados: init_uts_ns.name.nodename=0xc110d561
("(none)", legível via uname), scratch P=0xc118bd58 (zeros xfrm).
Evitar S=0xc111cba4 (adjacente a tracepoint). CONFIG_DEBUG_RODATA=y → todos os alvos
de escrita devem estar em .data/.bss (bss 0xc11d9000-0xc12d9000).
add_key (CONFIG_KEYS=y): negado por SELinux para shell. Morto.setxattr: kvmalloc(96)+copy_from_user acontece ANTES da
verificação SELinux → a dança de alloc é à prova de SELinux mesmo quando a chamada falha;
transitório (liberado no fim da syscall), bytes persistem em +4..95 (ptr de freelist
sobrescreve +0..3 = rblink, não usado por kbase_jit_free)Execução spray 700 em 2026-09-11: 5895 regiões sprayadas durante a pressão, JIT_FREE
em id=1 pendente completou em uma região recuperada, então
JIT_ALLOC(0x40, bin 0) percorreu jit_pool_head e retornou o VA da região sprayada
#4251 (0x142701000) — a região exata que o ponteiro pendente consumiu.
Kill do processo depois: teardown do kctx limpo, sem crash. Estágio 2 completo:
redirecionamento UAF determinístico com tipo de objeto + conteúdo controlados.
Reclaim do tipo região dá sobrevivência à dança legal mas jit_node é INIT'd self → sem primitiva de unlink. Precisa de bytes crus em +0x38/+0x3c:
*(N+4)=P com P=página de shellcode em userland (sem PAN!) —
candidatos: fops const são .rodata (DEBUG_RODATA) → alvo fn ptr não-const
em .data, ou cabeça da lista formats de binfmt, ou proc_handler de sysctl
(verificar gravabilidade da tabela). fallback: modprobe_path via escritas
encadeadas por byte (valores devem ser endereços graváveis — use alvos em forma de ponteiro)poc/stage3.c):
kern_table[pid_max].proc_handler @ 0xc1113f40 (.data gravável,
verificado via varredura de ponteiro-de-string + handler == proc_dointvec_minmax)b +8; W2 escreve N em P = campo handler)read /proc/sys/kernel/pid_max
(legível a partir do shell); roda no contexto da própria task → creds aplicam a nósinotify_handle_event faz kmalloc de name_len+0x1d,
bytes do nome (totalmente controlados, restrição sem NUL/slash) em event+0x1c;
name_len=60 → kmalloc-96; enfileirado → mantido; 4 instâncias → 4 eventos por
rename; SELinux-OK a partir do shell. Fake redesenhado sem NUL: cpu_alloc
aponta para S (nents@S+8 = 0 → mesma semântica de NULL)| variante | resultado |
|---|---|
Hipótese de trabalho: corrida de lixo-da-tempestade — entre o destroy worker liberar o slot (meio da tempestade) e o kill-child/quiet, atividade de reclaim residual toma o único-slot-livre do slab vítima com bytes não-payload. Spray de região (estágio 2) vence porque aloca continuamente DURANTE a tempestade; renames não conseguem.
iso): timing idêntico, trailing com
REGIÕES commit-0 + oráculo de reuso-de-pool do estágio-2 → HIT DO ORÁCULO
→ timing/alcançabilidade estão BEM; eventos são o problemacurrent->mm é de uma kernel thread → o design elegante
"apontar os ponteiros do fake para nosso próprio mmap de userspace" (sem PAN!)
FALHA de forma não-determinística. 5/5 crashes com um fake de outro modo perfeito.K=*(S+0x38) legível, *(K+0x1429c)==0 em runtime
(pula mm-atomics), K+0x141c8 gravável, D=*(K+4) → D+0x538
gravável, nents *(S+8) preferencialmente 0. Os 9 S-candidatos offline foram
validados contra bytes de ARQUIVO — drift de runtime (init de xfrm/tracepoint)
os torna não verificados. Cadeia errada = crash = reboot (~3 min de ciclo).A. Fake via spray de physmap (ret2dir, arm32 clássico sem-PAN): sprayar ~450MB de páginas de usuário cada uma contendo o padrão fake preparado para UM endereço adivinhado G (G&0xfff = 0x141 para bytes de nome sem NUL; S=G; K=G-0x141b4 para K+4 cair in-page; K+0x141c8/+0x1429c → G+0x10/+0xd4 in-page; D=G+0x300; stores sub-0 perdidos atingem RAM mapeada aleatória - inofensivo com nents=0). O spray também serve como pressão de evicção (anon sujo = não-evitável → só ~100-200MB extra necessários). Probabilidade ≈ 45% (hit de página) × ~50% (palavra K+0x1429c estrangeira é zero... se K+0x1429c mantido in-page pelo layout acima, probabilidade = apenas hit-de-página). Erro = crash = reboot, retry. B. Força bruta nos 9 S-candidatos estáticos (xfrm 0xc118b7ec primeiro, adjacente-a-tracepoint 0xc111cba4 segundo...): 1 reboot por candidato, payload de oráculo benigno primeiro, arma no hit. C. G exato baseado em pagemap (morto: PFNs mascarados) — não revisitar.
/tmp/opencode/ksrc2/kernel/mediatek/mt8163/4.9/ (arch/arm incl.
mustang.dtsi, mm/, fs/eventpoll.c, fs/notify) de ksrc/platform.tar.
Descobertas:
physmap_retouch() após o kill (faulta todas as páginas do spray
de volta antes do trailing/deref)Regiões reclaim: 3/3. Eventos: 0/~12 tentativas incluindo 28K alocações exclusivas com vantagem inicial e fake correto por construção. Se eventos reclaim com p_hit(G)≈0.35, cinco erros de pmap ≈ 11.6% — possível mas agora improvável (~10-15%). Ou os eventos estruturalmente não conseguem ocupar este slot (motivo desconhecido — mesmo cache, mesmo contexto, mesmo timing) ou nossos palpites de G estão sistematicamente errando (desvio de fronteira highmem, posicionamento do alocador).
vmalloc=496M slub_max_order=0 slub_debug=O loglevel=8 initcall_debug
Regiões ocupam o slot da vítima 3/3; eventos 0/13. Mesmo cache (provado em nível de código-fonte + disasm), mesmo contexto de processo, mesmas cpus fixadas, mesmo timing pós-kill, milhares de alocações com vantagem inicial exclusiva. Mecanismo desconhecido. Suspeitos restantes: correlação de taxa/frequência de alocação com rotação de lista parcial (ioctls de região ~1ms de intervalo vs renames de evento ~100µs de intervalo — direções opostas?), ou detalhes de ordenação da freelist do SLUB sob slub_max_order=0 que favorecem... incerto.
[+] uname.nodename="X?lhost" (was "localhost") <<< ORACLE HIT
**A cadeia completa de bytes brutos funciona no dispositivo ativo**: o evento recupera o
slot da região liberada → kbase_jit_free desreferencia nosso falso (S=página physmap do
spray kbm, G=0xc154b2a4) → o unlink executa nossas duas escritas.
Verificado repetidamente com o payload benigno (escrita em nodename).
### Cadeia de descobertas da sessão
1. As páginas kbm eram GFP_HIGHUSER → HIGHMEM, invisíveis ao physmap
(mali_kbase_mem_pool.c:164!) — corrigido pelo transbordamento de zonelist: spray de
260 × 2MB > highmem-free → o excedente cai em ZONE_NORMAL (physmap)
2. As primeiras tentativas de arma travaram: o alvo W1 N+4 era uma página USER —
o unlink roda em ctx kworker (sem mm) → fault. Corrigido embutindo o shellcode
ring-0 DENTRO do padrão physmap em page+0x600 (direct map RWX em
arm32 non-LPAE) — código residente no kernel, sem necessidade de ret2usr
3. Bug de offset do ctl_table: proc_handler está em entry+0x14, não +0x18 (o
scan original estava certo; meu define estava errado) — estava escrevendo extra1
4. Regressão por sobrepressão encontrada e revertida: orçamento kid 20×100MB +
trailing de 10s quebrou o reclaim; a config que funciona é 2 kids/200MB +
5s/+3200 trailing (benigno acertou 1/1 após reverter)
5. **diag2: W2 → &pid_max global (0xc1114d7c) → a leitura retorna nosso valor
(-1055861411 = 0xc110d55d como int32) — escrita + readback PROVADOS**
### O mistério restante (a um experimento de ser fechado)
diag1 com o endereço CORRETO do handler (0xc1113f3c): W1 dispara (nodename
muda), W2 deve ter executado (próxima instrução) — ainda assim as leituras de pid_max
retornam valores limpos → o campo handler que escrevemos não é aquele
pelo qual o inode despacha. diag3 (na fila; precisa de um boot com hit): W2 →
campo entry->data (0xc1113f2c) apontando para nodename — se a leitura então
mostrar nodename-bytes-as-int, nossa entry ESTÁ viva e apenas o offset do handler
está de algum modo errado; se não afetado, o inode usa uma cópia shadow da tabela
e caçamos a viva.
### Realidade da taxa de acerto
Cara ou coroa por boot (~25-40%), agrupado; vários boots safe-miss/crash
em sequência é normal. Aproximadamente 1 em 3-4 boots é um hit. Mantenha a config
que funciona EXATAMENTE (2 kids, 5s trailing, kbm de 260 regiões, G=0xc154b2a4).
### TODO da Sessão-9
1. Completar diag3 em um boot com hit (rolar até "W1 FIRED")
2. Se entry viva: re-checar o offset do handler empiricamente (escrever
o endereço de proc_dostring como handler via... N deve ser igual a um valor
útil — usar o unlink para escrever entry->data em vez disso e pivotar: por exemplo,
data=selinux_enforcing-adjacente...)
3. Se tabela shadow: localizar a viva — kallsyms não tem símbolos de dados;
candidatos: escanear o comportamento de /proc/sys, ou encontrar uma segunda região
ctl_table via o padrão de lista de headers em .data (entries com stride de 0x20
com handler=proc_dointvec_minmax e maxlen=4 — enumerar TODAS e
diag-escrever cada uma)
4. Classe de alvo alternativa que evita o dispatch inteiramente: ponteiros de função
em .data chamados de caminhos alcançáveis pelo shell (auditoria necessária)
5. A primitiva de escrita em si está PRONTA — qualquer alvo confiável de endereço
de kernel agora basta para root
## SESSÃO 9 — nf-WEAPON: reclaim+unlink+G-hit PROVADOS NA ARMA; resta apenas a caminhada do hook
### O novo design de trigger (substitui inteiramente o caminho do handler sysctl)
A ideia do usuário traduzida para memória de kernel: sem arquivo SUID (o sistema é
dm-verity RO; a primitiva escreve na RAM do kernel). Em vez disso: **hook netfilter
falso**. Este kernel tem o backport Android-common da NOVA
API nf_hook_entries, mas implementada como LISTA ENCADEADA (verificado por
disasm de nf_hook_slow + helper 0xc09897d4):
- `__ip_local_out(net, sk, skb)` carrega a CÉLULA de entries de
**[net+0x58c]**, armazena em state+0x1c, chama nf_hook_slow
- caminhada: `entry = *cell`; while(entry){ if (state->[4] <= entry->[0x20])
call entry->[0xc](entry->[0x14], skb, state); entry = entry->[0]; }
— ou seja, **fn@entry+0x0c, priv@entry+0x14, priority@entry+0x20,
next@entry+0x00**; state+4 = limiar INT_MIN (sempre passa)
- init_net = **0xc1104548** (CONFIG_NET_NS=n → sock_net() inlineia a
constante; 6678 refs movw/movt, campeão do histograma; confirmado de forma cruzada pelo
próprio literal de nf_hook_slow). ALVO: **[init_net+0x58c] = 0xc1104ad4**
- O hook LOCAL_OUT roda no contexto do processo EMISSOR → o commit_creds(prepare_kernel_cred(0))
do nosso hookfn dá root ao processo que enviou o
pacote. Trigger = sendto(127.0.0.1:9) UDP.
### Layout do modo nf (poc/stage3.c, modo `nf 200`)
- páginas de padrão kbm (relativas à página, fonte única de verdade — a antiga
arma tinha TRÊS bugs agora corrigidos: proc_handler@+0x14 não +0x18; alvo W1
deve ser memória de KERNEL (ctx kworker, sem mm); código embutido em
page+0x600 vs entry G+0x600=page+0x8a4 incompatíveis):
- +0x2ac/+0x2bc/+0x2c0/+0x2dc/+0x3a8: cadeia phy-alloc falsa (inalterada)
- +0x600: hookfn nf_code (store 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 (cai na nossa página), W2: *(init_net+0x58c)=cell
### Instrumentação AUTODIAGNOSTICÁVEL (kbm_scan_for)
Os mapeamentos de CPU do kbm são mantidos; após o free escaneamos cada página
sprayada em busca de uma palavra conhecida:
- scan(HOOKS_PTR_ADDR) em page+0x744 → prova reclaim + unlink + revela
qual página física sustenta o palpite de G
- scan(0x600d600d) em page+0x7f0 → prova que o hookfn EXECUTOU
(nf_code escreve este marcador como sua 2ª ação)
### A EXECUÇÃO QUE IMPORTA (2026-09-11, fim da sessão 9)```
[+] W1 CONFIRMED: region 135 page+0x185000 <- 0xc1104ad4 (reclaim + G hit!)
[+] nf trigger done, uid=2000 euid=2000
A cadeia completa de armas disparou em um boot real: o evento recuperou o slot, o fake executou, o unlink executou, a página G-guess (0xc154b000) era genuinamente nossa (região 135 página 389). W2 = *(0xc1104ad4)=cell é a instrução adjacente — ela deve ter executado. No entanto, o UDP sendto não nos deu root → a falha está DENTRO do caminho do hook: semântica do walk, encanamento de [state+0x1c], comparação de prioridade, ou os campos de entrada. (O experimento com marcador para discriminar hookfn-executou-vs-não foi adicionado; apenas boots com crash antes do fim da sessão — ainda sem dados limpos.)
pmap 200) é a verificação de sanidade do ambiente (4/4 acertos quando
quente; crash-miss quando frio)nf 200 em boots com atraso de assentamento até que a linha W1-CONFIRMED
apareça, então ler a linha MARKER:
a. marcador PRESENTE, uid!=0 → as creds do shellcode falharam (verificar
endereços prepare_kernel_cred/commit_creds; codificações blx)
b. marcador AUSENTE → o walk nunca nos chamou: verificar com um SEGUNDO
marcador escrito por... próximos diagnósticos: hookfn que APENAS escreve
o marcador e retorna 1 (sem creds) — se ainda ausente:
ls -la; deixar arquivos marcadorespoc/stage3.c: pin/root/drain{,2,3,4,5}/iso — arma completa +
oráculo + harness de isolamento, forense de ponto de crash com O_SYNCinit_net errado: 0xc1104548 é __stack_chk_guard (o histograma
movw/movt foi poluído por cargas do stack-canary — 2025 construiu todo o plano nf sobre
ele). init_net real = 0xc1185040 (confirmado: ip_send_skb(net,...) chamado
com este literal; ~994 refs todas na pilha de rede). Cell IPv4 LOCAL_OUT =
init_net+0x58c = 0xc11855cc.nf_iterate trata [init_net+0x58c] como o
próprio ponteiro nf_hook_ops — ele lê fn@+0xc, priv@+0x14, prio@+0x20
diretamente desse valor. A entrada fake da sessão-9 vivia em
PM_PAGE+0x700 com a cell apontando para ela (campos next/ não usados
na cell → → crash). A entrada fake DEVE viver
(0xc154b740). Com isso corrigido, a chamada do hook
foi provada (modo = SAFE_FN processa todos os 200 sendtos limpidamente).commit_creds(prepare_kernel_cred(0)) / override_creds(&init_cred) dá
uid 0 mas cai no SID SELinux kernel, que esta política do Fire OS
não permite escrever em /data ou /sys/fs/selinux/enforce (verificado: EACCES).
O SID real de init (7) também é negado (teste com struct cred fake). enforcing_setup
é __init (liberado → crash). O atomic_sub de mark_reclaim precisa de nents=1 o que
quebra a saída antecipada de shrink_cpu_mapping.
A vitória: o processo do exploit mantém os mapeamentos de CPU do kbm, então a entrada nf fake pode ser reescrita no lugar entre pacotes:
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 para {fn=commit_creds, priv=&init_cred}.commit_creds(&init_cred) na task do remetente → uid 0 com
um SELinux permissivo → root utilizável, tudo em uma única recuperação, sem necessidade de cadeia.[+] 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 pausado é `Uid: 0 0 0 0`,
`CapEff: 3fffffffff`.
- `/data` está montado como **nosuid**, então um `su` setuid não pode funcionar. Um pequeno
`rootshell` (envia UDP → `commit_creds` em si mesmo → `execl sh`) fornece um
shell root interativo: `uid=0(root) context=u:r:kernel:s0`.
- Root pode ler/escrever `/dev/block/by-name/*` (`dd if=boot ...` OK).
### Estado do código do Estágio 3 (`poc/stage3.c`)
- modos: `nf <mb> [probe|uprobe|oc|chain|fc <sid>|notrig|selroot]`
- `selroot` é a arma funcional. Estáticos-chave: `init_net=0xc1185040`,
`HOOKS_PTR_ADDR=0xc11855cc`, `ENFORCING_ADDR=0xc1213ea8`,
`ZERO_GADGET=0xc01d503c`, `commit_creds=0xc014993c`,
`init_cred=0xc1114f54`.
- Pós-exploit são syscalls diretas (sem `system()`); mantenha o kctx vivo
(`pause()`) para evitar crash de teardown.
### Restante (Estágio 4/5)
- Persistência entre reinicializações (verity / imagem de boot / recovery), já que o sequestro
de célula + SELinux permissivo são apenas em runtime e reexecutar o exploit exige
o coin flip de ~1/3 de reclaim.
- `su` precisará de um home não-nosuid (`/system`) ou de um launcher que reative.
## SESSÃO 11 — RECON DE PERSISTÊNCIA (Track B + Track A) e a transferência de RE
O objetivo era root persistente. Duas trilhas foram definidas:
- **Track B**: desabilitar o boot verificado (dm-verity / SELinux) para que `/system` possa ser patchado.
- **Track A**: reexecutar o exploit no boot.
Ambas se reduzem ao mesmo bloqueio: **fazer o LK tratar o dispositivo como `eng`/`unlocked`.**
### Fatos do boot verificado (build exata)
- Bootloader travado, AVB `green`, `ro.boot.unlocked_kernel=false`, `ro.boot.secure_cpu=1`,
`rpmb_state=1`. Bootrom patchado (sem BROM); preloader apenas via CMD short.
- `/system` é montado pelo **Android dm-verity a partir da cmdline do kernel construída pelo 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 chamado `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 partições, sem `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) = armazenamento IDME.
### Achados estáticos do LK (UFBL)
Cabeçalho do `lk.img`: `88 16 88 58 | 00052974 | "LK"`; tabela de vetores ARM em 0x200, restante Thumb-2,
position-independent/relocado (literal pools usam `ldr+add pc`, então disasm ingênuo relativo à base falha).
Strings relevantes (offsets de arquivo): `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`.
**Conclusão: o LK condiciona os efeitos de segurança de `fos_flags`/`dev_flags` a eng/unlocked.**
### Armazenamento IDME (eMMC **boot1**) — gravável, persistente, lido pelo LK e pelo Android
- Magic `beefdeed` + `"2.1\0"` + count(0x19=25) em 0x0; itens a partir de 0x10.
- Formato do item: `char name[16]; u32 size; u32 type(=1); u32 magic(=0x124); u8 data[size] (pad4)`.
- Offsets dos itens (pristinos): `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`. Os valores são ASCII (flags são **strings hex**).
- Leitura em runtime: `/proc/idme/<name>` (somente leitura). Valores do último boot ficam em cache; uma escrita em boot1 tem
efeito no próximo boot. O caminho de escrita exige limpar `/sys/block/mmcblk0boot1/force_ro` (root).
- **Confirmado que o LK lê boot1**: alterar `serial` mudou `ro.boot.serialno` no próximo boot.
Mas o LK **trunca serial para 16 bytes** e ignorou `fos_flags=0x80`, `dev_flags=0xff`,
all-ones, etc. — verity/selinux/`prod` inalterados. Então injeção de cmdline via serial falha.
### Consumidores dos flags IDME no lado Android
- `/init.fosflags.sh` (serviço `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: definir flags tem efeito (`sys.usb=adb`, `noadbauth=1`).
- **adbd** (ARM ET_EXEC não stripped; `.text` VA 0x8160 / arquivo 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` (sem gate)
- `amzn_is_dev_unlocked` @0x2d5fc = `/proc/cmdline` contém `androidboot.prod=0` **ou**
`androidboot.unlocked_kernel=true`
- `fos_read_debug_flags` @0x2d724 lê `/proc/idme/<name>` e faz parse **hex**
- `restart_root_service` @0xcb74 / `restart_unroot_service` @0xcc64
- strings: `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`) — então mesmo com o
gate de auto-root satisfeito, a checagem de prod do AOSP condiciona o caminho do comando.
### Por que o root da Sessão 10 não persiste
- SELinux permissivo + sequestro de célula + root são apenas em runtime.
- `/data/metrics` é uma **vpartition**: `/system/bin/vpartition.sh` monta `/data/vp/metrics.img`
(ext4, non-nosuid/noexec) em `/data/metrics` a cada boot; `su` escrito lá **não**
sobrevive a reinicialização. (Também é por isso que o `su` setuid deu uid 0 mas **zero caps**.)
### Track A (reexploit no boot) — bloqueado
- Nenhum trigger `.rc` do init executa código controlável (imports todos verificados; triggers `persist.*` apenas
`start` serviços fixos; scripts em `/system`/`/vendor`).
- Serviços root leem configs de `/data` mas nunca executam a partir delas (`perfmonitord`, `amazonfiled`,
`vpartition.sh`, `kisd`, …).
- Único executor no boot = um **app**, mas o footprint pausado do exploit é **VmRSS 534 MB**
(spray `kbm`) → lmkd o mata; além disso, um reclaim perdido causa panic (`PANIC_ON_OOPS`) → bootloop.
- **Auto-root do adbd** existe mas é condicionado à cmdline construída pelo LK (`prod=0`/`unlocked_kernel=true`).
### Conclusão / próximo alvo (escolhido: RE do Track B)
Tudo depende de fazer o LK reportar `eng`/`unlocked`. Ao alcance:
`androidboot.prod=1|0` e `androidboot.unlocked_kernel=false` são definidos pelo LK. Reverter o LK para encontrar:
1. onde ele lê `fos_flags`/`dev_flags`/`usr_flags` (os itens `K*`) e o gate exato;
2. a determinação de `prod`/`unlocked` (item IDME? buildvariant? resultado de `amzn_verify_unlock`?);
3. `amzn_verify_unlock` (verificação RSA libtomcrypt) para um bypass ou um caminho fraco de unlock_code/version;
4. o armazenamento `seccfg`/`para`/`ENV_v1`(LK_ENV) (não está em nenhuma partição dumpada — talvez protegido por tee);
5. preloader (`boot0`, `EMMC_BOOT`) para um bug.
Se qualquer um destes nos permitir definir eng/unlocked (persistentemente, via boot1 ou escrita em partição bruta), então
`FOS_FLAGS_DM_VERITY_OFF` desabilita o verity de system(root) e `/system` pode ser patchado persistentemente.
### Artefatos (desta sessão)
`/tmp/opencode/mustang-dumps/` (pode ser limpo no reboot do host): `lk.img`, `boot1.img` (pristino),
`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` (em /tmp);
o repo tem `run.sh`, `poc/stage3.c` (`selroot`), `poc/su.c`, `rootcmd.sh`.
### Comandos úteis```
# 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: fazer o LK tratar o dispositivo como eng/unlocked, ou encontrar um bug no preloader/LK,
para que verity/SELinux possam ser desativados de forma persistente. Resultado: revertido o caminho
de código relevante do LK de ponta a ponta; a inversão não é alcançável pelos armazenamentos disponíveis.
Nenhum dispositivo foi brickado; o único experimento com boot1 foi revertido ao estado original.
lk.img começa com um pequeno stub ARM (arquivo 0x200). O realocador em 0x224
copia de 0x200 para um destino literal e desvia para uma entrada literal:
Portanto, endereço em tempo de execução = 0xFF400000 + offset no arquivo para offsets >= 0x200.
Tudo após o stub é Thumb-2, independente de posição. As strings são construídas
com ldr rT,[pc,#imm] (offset T1 = imm8*4; offset ldr.w = imm12) seguido de
add rT, pc; o alvo é (add+4) + *pool. Um scanner robusto que sobrevive
ao stub ARM e aos pools de literais foi adicionado como tools/lk_xref.py (lida com formas de 16 e
32 bits, varre a cada 2 bytes). Todos os offsets abaixo são offsets no arquivo;
adicione 0xFF400000 para endereços em tempo de execução.
0x20b4 lê o item IDME unlock_code (0x400 bytes, todos zero nesta
unidade) e executa amzn_verify_unlock (0x222c -> 0x20f0). Essa função aciona
o libtomcrypt (dezenas de caminhos /features/libtomcrypt/src/pk/asn1/der/... e
verificação RSA), e a imagem incorpora o material do certificado:
Sunnyvale / Amazon Lab126 / "$Common Kernel Signing Engineering CA0" em
0x317d9+, além dos 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).
Não há atalho de código vazio / comprimento / versão: verify(zeros) != 0, portanto
(confirmado em ). Inverter
ou requer ou um
válido assinado pela Amazon (chave privada indisponível) ou um bug de execução de código no
verificador. Nada explorável (limites/tamanho) foi encontrado estaticamente em
0x20b4/0x222c/0x20f0. =>
As flags de segurança são lidas através do getter em 0x57c. Teste empírico:```
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` é `FOS_FLAGS_DM_VERITY_OFF`; o gate descodificado teria desativado
o verity **se** o getter o tivesse devolvido. Não o fez. O gate está ativo, não é
código morto: o seu sentinela de cache de uso único é `-1` na imagem
(`*(u32*)0x50c74 == 0xffffffff`), pelo que a função realmente executou o
caminho `check_flag("fos_flags",0x80)` e obteve 0. Portanto, o getter (pelo menos
no momento da proteção do verity) **não** está a ler os itens IDME do boot1.
O outro armazenamento candidato é o **LK env**, carregado de uma partição literalmente
chamada `"para"` (loader 0x12fd4, magic `ENV_v1`, checksum @0x3ffc). A própria
tabela de partições do LK (0x4fcc0..0x50340) lista preloader/proinfo/nvram/protect1/
protect2/persist/seccfg/secro/**para**/logo/custom/expdb/tee1/tee2/metadata/
system/cache/userdata — mas a GPT real do tablet tem **apenas 16 entradas**, todas
do 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
Não há nenhuma partição para, seccfg, nvram, protect ou persist neste
produto (e os dumps de PMT/pmt.img são todos zeros). Portanto, o env do LK está vazio,
as chaves Kfos_flags/Kdev_flags nunca existem, e todas as verificações de
fos_flags/dev_flags resultam em 0 — independentemente do que os itens do IDME
contêm. Os itens do IDME do boot1 são consumidos pelo Android (/init.fosflags.sh, adbd,
/proc/idme/*) mas não pelos portões de segurança do LK.
unlocked_kernel requer um unlock_code assinado pela Amazon (RSA/libtomcrypt,
CA embutida). Não é forjável offline; nenhum bug no verificador foi encontrado. Bloqueio rígido.DM_VERITY_OFF / selinux=permissive são consumidas do env do LK
(para/ENV_v1), que não existe neste GPT. O fos_flags do IDME é
empiricamente ignorado pelo LK (0x80 persistido, verity permaneceu eio). Bloqueio
rígido a menos que a tabela de partições seja modificada.fos_flags=0x80 bem-sucedido apenas definiria androidboot.veritymode= disabled e um root= não-dm-0; não desbloquearia, e o SELinux
ainda precisaria de dev_flags do mesmo env ausente para ficar permissivo.para/ENV_v1: adicionar uma entrada GPT chamada para (GPT primário
userdata
(userdata termina no LBA 0x3a3dfde; disco = 30535680 setores), então criar um env com
fos_flags=0x80 e dev_flags=0x40 (checksum em +0x3ffc = soma de bytes sobre
0x3ffc). Esta é a única rota restante para verity-off. Riscos: corromper
o GPT primário/de backup pode brickar; e não foi provado que o portão de verity
realmente lê para (apenas que não é o IDME do boot1).boot0/EMMC_BOOT): não revertido nesta sessão. Escrever
boot0 é proibido até que exista uma cópia pristine e um caminho de recuperação.tools/lk_xref.py — resolvedor de xref de strings do LK independente de base./tmp/opencode/mustang-dumps/lk.img, boot1.img (pristine),
boot0.img, mbr.img (GPT), pmt.img (todos zeros)./tmp/opencode/s12/boot1_f80.img; dispositivo restaurado para boot1 pristine
(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
## SESSÃO 13 — o preloader é acessível afinal (`1949:20ff` = preloader MTK, transporte HID)
Enquanto desligado e conectado ao USB, o tablet enumera como **`1949:20ff`**
(`Lab126`) — *não* Android e *não* a bootrom `0e8d:0003`. Descritor:```
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)
Identificação. 0x20FF está listado como "MTK Preloader" no
config/usb_ids.py do mtkclient (sob o VID da MediaTek 0x0e8d: 0xe8d:{0x0003 Brom, 0x2000/0x2001/0x20ff/0x3000 Preloader}). A Amazon manteve o PID do preloader e
alterou o VID para 0x1949, e apresenta-o como um par de endpoints HID com um
descritor de relatório fictício. Portanto, este é o MediaTek preloader / modo USBDL, um estágio
abaixo do LK — alcançado aqui por desligamento + conexão, não pelo curto do CMD.
As strings do descritor "HID"/"HID Interface" não estão presentes em lk.img,
boot0.img, boot.img ou nos outros dumps, ou seja, o modo é produzido por um
componente que não foi dumpado (bootrom/TEE) ou é montado em tempo de execução.
Por que isso importa. O preloader da Amazon usado pelo aftv2-tools expõe
comandos integrados, sem Download-Agent, sobre este exato fluxo de bytes:```
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 acessar o controlador MSDC
(base `0x11230000` no MT8173; verifique para MT8163) e ler/gravar **blocos eMMC
brutos** sem DA e, portanto, sem AVB/verity no caminho. Se o preloader do mustang
aceitar 0xD1/0xD4, esse é um caminho direto para desbloqueio persistente (patch em `boot` /
`lk`), independente do código de desbloqueio RSA e do ambiente LK ausente.
### Ferramentas adicionadas (necessário root; aplique chmod no nó USB primeiro)```
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 é a sonda mínima apenas de handshake;
tools/mtk_preloader_hid.py é o transporte completo (handshake/read32/
write32; write32 está protegido e não deve ser usado até que o mapa de
registradores da eMMC seja confirmado).
read_mmc), então aplicar patch em boot.img/lk a partir do preloader e reiniciar.amzn_verify_unlock nem do gate de ambiente do LK.pi_blocked_onfutex.c:1975 passa this->rt_waiter,
declarado em futex_wait_requeue_pi em futex.c:2880) → o waiter marca seu próprio frame liberado
via select do arm32 (nr 142) fd_setsDEBUG_RT_MUTEXES desligado
→ 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 (string auto-localizada em
vmlinux; KALLSYMS_ALL desligado, então símbolos de dados precisam deste truque) → exec de binfmt desconhecido → script
root (setenforce 0, desabilitar OTA, su)refs/: NebuSec/CyberMeowfia (original), GhostLock-5.10 (porta para Fire OS 8,
trigger ARM 32-bit completo em src/exp32/), ghostlock-...-4.19-k40 (porta Android Qualcomm 4.19)selrootselinux_state.enforcingcommit_creds(&init_cred)uid=0jcnr_extresinfo->gpu_alloc_addr (um GPU VA que você deve
pré-alocar e passar)| objeto evictável | pressão | resultado |
|---|
| nenhum | 900 MB | sobreviveu |
| nenhum | 1300 MB | pânico (bug de lowmem do sistema — não relacionado) |
| região normal + DONT_NEED | 700 MB | sobreviveu |
| região JIT + DONT_NEED | 700 MB | pânico no caminho de reclaim |
kernel/config-* — dump de /proc/config.gz do dispositivo em execuçãoksrc/ — código-fonte OSS da Amazon (platform.tar + árvore midgard-r26p0 extraída)/tmp/opencode/mustang_ota.bin (sha256 6068515a… corresponde ao fireos-archive)
e tarball de código-fonte do kernel de 2.2 GB mantido em ~/Desktop/amazon-mustang/ksrc/kernel/mediatek/mt8163/4.9/drivers/misc/mediatek/gpu/gpu_mali/mali_midgard/midgard-r26p0/| sprayers de rename concorrentes durante a tempestade |
| renames travam (journal/GFP_NOFS) → 128 total → deref de lixo |
| pré-drenar 12K eventos + pressão + pequeno trailing | crash no deref |
| + kill-child-na-evicção (poll de 10ms) | crash no deref |
| + ciclo-de-vida fixado em cpu0 (drain3) | crash no deref |
| pressão escalonada (drain4 v1) | filhos liberaram memória ao sair → sem evicção (caminho legal validado) |
| execução | resultado |
|---|
| pmap G=c2a412a4 400MB | crash no deref |
| pmap G=c2f4b2a4 480MB | crash; +0 renames pós-kill (480MB sufoca o fs) |
| iso2 (spray + regiões + oráculo) | ACERTO DE REGIÃO — spray não quebra o reclaim |
| mix v1 (eventos+regiões concorrentes) | crash; confundido (threads de evento em spin) |
| pmap G=c2a7d2a4 350MB + retouch | crash; +4452 renames OK |
| mix2 (sequencial: 2s eventos ENTÃO regiões) | crash; +7126 renames (28K alocações de evento), 2715 regiões |
fnfn=0PM_PAGE+NF_CELL_OFFprobekernel_x_end: arch/arm/mm/mmu.c
map_lowmem() mapeia lowram abaixo do texto do kernel como MT_MEMORY_RWX, mas tudo
acima de kernel_x_end como MT_MEMORY_RW → PMD_SECT_XN (linha 509). Shellcode
embutido em 0xc154b600 dá prefetch-abort. O payload deve ser um ponteiro de
função real do kernel, não código no physmap.| literal (off no arquivo) | valor | significado |
|---|
| 0x270 | 0xFF4002F8 | scratch de str r4,[r6] |
| 0x274 | 0xFF40027C | destino (base+0x27C) |
| 0x278 | 0xFF54A440 | fim da cópia (incl. BSS) |
| 0x27C | 0xFF400484 | ponto de entrada |
| offset | função |
|---|
0xdf7c | is_secure_or_prod() -> byte[ [[g]+0 ] + 0x163 ]; g = global @0x52838. 1 nesta unidade. |
0x20b4 | verify_stored_unlock() = memset(buf,0,0x100); lê IDME/env unlock_code (0x100) via 0x57c; bl 0x222c; retorna (verify==0). |
0x222c / 0x20f0 | amzn_verify_unlock(code,len) — verificação RSA/PKCS#1 do libtomcrypt (ver abaixo). |
0xda3e | is_unlocked() = is_secure_or_prod() && verify_stored_unlock(). |
0x29a28 | is_verity_disabled() = (fos_flags & 0x80) && !(is_secure_or_prod() && verify_stored_unlock()); armazenado em cache no global @0x50c74. |
0x29974 | Construtor da cmdline do SELinux: dev_flags & 0x20 -> androidboot.selinux=enforce, dev_flags & 0x40 -> ...=permissive (cada um controlado por byte[+0x162]). |
0x118xx/0x11bxx | construtor da cmdline do kernel (unlocked_kernel, prod=1/0, verifiedbootstate, rpmb_state, secure_cpu, versões, root=). |
0x27af8 | Portão da UART: fos_flags & 0x4 -> printk.disable_uart=0, senão =1. |
0x12fd4 | Carregador de env do LK: partição "para", 0x4000 bytes, magic ENV_v1, checksum = soma dos bytes sobre 0x3ffc comparada à word @0x3ffc. |
0x1efd0 | busca de partição por nome (usada para "para", "boot", ...). |
0x57c | despachante de getter através do slot de callback @0x58218; slots @0x58200..0x5821c são registrados a partir de uma tabela em 0x5a8-0x734. |
0x2a19c | fastboot oem unlock: bl 0x222c(code,len); em caso de sucesso grava unlock_code (0x100) via 0x408. |
unlocked_kernel=false/proc/cmdlineandroidboot.unlocked_kernelandroidboot.produnlock_code