
Análise aprofundada e prova de conceito para CVE-2026-27280, uma escrita fora dos limites no dng_render_task::ProcessArea do Adobe DNG SDK, acessível por meio da API de decodificação de imagens do Android, com causa raiz, avaliação de explorabilidade e análise de lacunas para RCE.
dng_render_task::ProcessArea (Adobe DNG SDK / Android)Alcançada a partir de um ficheiro DNG comum através da API pública de descodificação de imagens do Android, e
reproduzida num Galaxy S25 usando o próprio /system/lib64/libdng_sdk.so do telemóvel.
DNG_REQUIRE(srcArea.Size() == dstArea.Size()) + indexação de srcArea)#12 AImageDecoder_decodeImage /system/lib64/libjnigraphics.so
#11 android::ImageDecoder::decode /system/lib64/libhwui.so
#09 SkCodec::getPixels /system/lib64/libhwui.so
#08 SkRawCodec::onGetPixels /system/lib64/libhwui.so
#07 dng_render::Render() /system/lib64/libdng_sdk.so
#06 SkDngHost::PerformAreaTask /system/lib64/libhwui.so
#00 dng_render_task::ProcessArea /system/lib64/libdng_sdk.so <-- vulnerável
ProcessArea mapeia um buffer temporário do tamanho de um tile sobre srcArea, mas indexa-o com coordenadas de dstArea.
Como srcArea = dstArea + fSrcOffset e fSrcOffset é o DefaultCropOrigin do ficheiro — diferente de zero em todos os DNG reais de câmaras — o ponteiro de destino é
deslocado para fora da alocação por fSrcOffset * rowStep * pixelSize, e a leitura de retorno da máscara
escreve nesse local. É um deslocamento, não um overflow de inteiros, por isso não precisa de uma alocação enorme:
é por isso que é alcançável num telemóvel.
01-vulnerability/ as versões vulnerável e corrigida de dng_render.cpp, e o diff 2471->2502
02-poc/ geradores, carregadores, sondas (fontes) + binários arm64 pré-compilados
03-samples/ os ficheiros DNG
04-evidence/ logs ASAN, tombstones do dispositivo, saída das sondas
05-analysis/ causa raiz, gerador, medições de vizinhos, análise de lacunas para RCE
O trabalho que vai além do veredito anterior: refuta duas das suas afirmações por
medição, fecha a lacuna "um ponteiro corrompido é usado", e identifica exatamente por que a
cadeia para. Leia 06-rce-lab/README.md primeiro; os ficheiros evidence/*.md são os
relatórios por experiência, e src/ + apk/ são os harnesses reproduzíveis.
adb push 02-poc/bin-arm64/poc_aimg 03-samples/trigger_natural_croporigin.dng /data/local/tmp/
adb shell "cd /data/local/tmp && chmod 755 poc_aimg && ./poc_aimg trigger_natural_croporigin.dng"
# -> Segmentation fault, tombstone frames inside libdng_sdk.so
Recompilar os carregadores arm64 requer os próprios flags do dng_sdk do Android (de Android.bp), caso contrário os
layouts das structs não corresponderão à biblioteca incluída:
-DUNIX_ENV=1 -DqDNGBigEndian=0 -DqDNGThreadSafe=1 -DqDNGUseLibJPEG=1 \
-DqDNGUseXMP=0 -DqDNGValidateTarget=1 -DqDNGUsingSanitizer=1 -DqLinux=1 -DqAndroid=1
Isto corresponde a CVE-2026-27280 (DNG SDK 1.7.1 2471 e anteriores, CWE-787, CVSS 3.1 7.8 ALTO), publicado em 2026-03-10 — correspondido por intervalo de versões e classe, não confirmado pela Adobe ao nomear a função.
Provado, no caminho real de descodificação do Android, num dispositivo com patch até 2026-01-01:
escrita fora dos limites com destino escolhido pelo atacante (granularidade de 4 bytes, em ambas as
direções, independente de ASLR); alcançável a partir de um DNG simples através de AImageDecoder,
demonstrado a matar a Galeria Samsung no seu carregador de miniaturas; corrupção silenciosa e determinística
de vizinhos; um ponteiro corrompido a chegar a free() (N1 fechado); um
heap-spray expresso pelo formato DNG (ImageStats × SubIFD); a escrita apontada para
esses objetos pulverizados, 6/10 vs 0/10.
Não é RCE, no caminho Android, e por uma razão raiz: o código pode escrever um único
valor, 0x3F800000. Tudo o resto decorre disso — a escrita é contígua (sem stride
sobre cabeçalhos Scudo), não há zero para escrever (sem corrupção de comprimento/refcount), e a
sobrescrita parcial que preserva tags tem exatamente um offset de alvo fixo. Um valor é
uma tentativa contra uma reserva aleatória de 256 MB, e nenhuma quantidade de volume de spray ou
concorrência adiciona tentativas. No SDK de desktop o conjunto gravável é de 256 valores → 256 tentativas
numa banda de ~67 MB, que é por que o 7.8 está correto lá.
Retirado do veredito anterior (05-analysis/05-exploitability-verdict.md),
falsificado por medição: "um descodificador real não oferece primitiva de spray" e "a
sobreposição não é orientável de dentro de uma única descodificação". O bloqueio não é PAC, BTI ou
os checksums do Scudo — é o único valor gravável, uma propriedade do código que a correção 2502 da Adobe
remove na raiz ao indexar com srcArea.
Nenhum exploit funcional foi construído, e nenhum é publicado aqui.
| ficheiro | o que faz |
|---|
trigger_natural_croporigin.dng | geometria natural (DefaultCropOrigin = 8,8); provoca crash no AImageDecoder |
benign_reference.dng | mesmo ficheiro com origem 0 — descodifica sem problemas, usado para sondar o heap |
silent_corruption.dng | classe de tamanho pequena: escreve fora dos limites sem crash (5/5) |
far_write_crash.dng | deslocamento grande: falha visivelmente |
| ficheiro | conteúdo |
|---|
01-root-cause.md | o bug, a correção, a forma da escrita |
02-dng-generator.md | como o ficheiro de acionamento é construído, os dois parâmetros de ajuste, armadilhas |
03-neighbours.md | números de controlabilidade, o que fica ao lado do buffer, mitigações |
04-what-is-missing-for-rce.md | o que está provado (P1-P8), o que não está (N1-N3), os passos restantes |
05-exploitability-verdict.md | o veredito anterior — substituído por 06-rce-lab, mantido para registo |
| ficheiro | conteúdo |
|---|
EXP1-stride-vs-scudo.md | stride vs cabeçalhos Scudo: possível em abstrato, impossível no caminho Android (planes==1) |
EXP2-N1-corrupted-pointer-used.md | um ponteiro corrompido chega a free() — N1 fechado |
EXP2-real-process.md | a escrita no processo real da Galeria Samsung, nada plantado |
EXP-imagestats-spray.md | o heap-spray que o veredito anterior chamou de impossível, expresso pelo formato DNG |
EXP-aimed-spray.md | apontar a escrita para objetos pulverizados: 6/10 vs 0/10 |
EXP-small-class-in-app.md | ponteiros de código ao alcance dentro de um processo real de app |
EXP-band-commit.md / EXP-concurrency.md | por que confirmar o alvo falha: um valor gravável = uma tentativa |
EXP-write-read-mismatch.md | um segundo defeito + a auditoria completa do código-fonte de ProcessArea |
EXP-value-1.0f.md | a rota do 1.0f-como-dado, fechada estruturalmente |