Uma ferramenta baseada em Frida para rastrear o uso da API JNI em aplicativos Android.
As bibliotecas nativas contidas nos aplicativos Android geralmente usam a API JNI para utilizar o tempo de execução do Android. Rastrear essas chamadas através da engenharia reversa manual pode ser um processo lento e doloroso. jnitrace funciona como uma ferramenta de rastreamento de análise dinâmica semelhante a Frida-Trace ou Strace, mas para o JNI.
A maneira mais fácil de correr com jnitrace é instalar usando o PIP:
pip install jnitrace
Após uma instalação PIP, é fácil executar jnitrace :
jnitrace -l libnative-lib.so com.example.myapplication
jnitrace requer um mínimo de dois parâmetros para executar um rastreamento:
-l libnative-lib.so -é usado para especificar as bibliotecas para rastrear. Esse argumento pode ser usado várias vezes ou * pode ser usado para rastrear todas as bibliotecas. Por exemplo, -l libnative-lib.so -l libanother-lib.so ou -l * .com.example.myapplication - é o pacote Android a ser rastreado. Este pacote já deve estar instalado no dispositivo.Argumentos opcionais estão listados abaixo:
-R <host>:<port> - é usado para especificar a localização da rede do servidor FRIDA remoto. Se A: não for especificado, localhost: 27042 é usado por Deafult.-m <spawn|attach> - é usado para especificar o mecanismo de anexo FRIDA para uso. Pode ser gerado ou anexar. Spawn é a opção padrão e recomendada.-b <fuzzy|accurate|none> - é usado para controlar a saída de backtrace. Por padrão, jnitrace executará o backtracer no modo accurate . Esta opção pode ser alterada para o modo fuzzy ou usada para interromper o backtrace usando a opção none . Veja o Frida Docs para uma explicação sobre as diferenças.-i <regex> - é usado para especificar os nomes de métodos que devem ser rastreados. Isso pode ser útil para reduzir o ruído em aplicativos JNI particularmente grandes. A opção pode ser fornecida várias vezes. Por exemplo, -i Get -i RegisterNatives incluiria apenas métodos JNI que contêm Get ou Registratings em seu nome.-e <regex> - é usado para especificar os nomes de métodos que devem ser ignorados no rastreamento. Isso pode ser útil para reduzir o ruído em aplicativos JNI particularmente grandes. A opção pode ser fornecida várias vezes. Por exemplo, -e ^Find -e GetEnv excluiria dos resultados todos os nomes de métodos JNI que começam a encontrar ou conter Getenv.-I <string> - é usado para especificar as exportações de uma biblioteca que deve ser rastreada. Isso é útil para bibliotecas nas quais você deseja rastrear apenas um pequeno número de métodos. As funções que o JNitrace considera exportadas são qualquer função que seja diretamente chamada do lado Java, como tal, que inclui métodos vinculados usando registradores. A opção pode ser fornecida várias vezes. Por exemplo, -I stringFromJNI -I nativeMethod([B)V pode ser usado para incluir uma exportação da biblioteca chamada Java_com_nativetest_MainActivity_stringFromJNI e um método ligado usando nomes de registro com a assinatura do nativeMethod([B)V .-E <string> é usado para especificar as exportações de uma biblioteca que não deve ser rastreada. Isso é útil para bibliotecas em que você tem um grupo de chamadas nativas ocupadas que deseja ignorar. As funções que o JNitrace considera exportadas são qualquer função que seja diretamente chamada do lado Java, como tal, que inclui métodos vinculados usando registradores. A opção pode ser fornecida várias vezes. Por exemplo, -E JNI_OnLoad -E nativeMethod excluiria do rastreamento a chamada de função JNI_OnLoad e quaisquer métodos com o nome nativeMethod .-o path/output.json - é usado para especificar um caminho de saída onde jnitrace armazenará todos os dados rastreados. As informações são armazenadas no formato JSON para permitir o pós-processamento posterior dos dados de rastreamento.-p path/to/script.js - O caminho fornecido é usado para carregar um script FRIDA no processo de destino antes que o script jnitrace tenha carregado. Isso pode ser usado para derrotar o código anti-frida ou anti-fuga antes do início jnitrace .-a path/to/script.js - O caminho fornecido é usado para carregar o script FRIDA no processo de destino após o carregamento jnitrace .--hide-data -Usado para reduzir a quantidade de saída exibida no console. Esta opção ocultará dados adicionais que são exibidos como Hexdumps ou como referências de string.--ignore-env -Usando esta opção, ocultará todas as chamadas que o aplicativo está fazendo usando a estrutura JNIENV.--ignore-vm -O uso desta opção ocultará todas as chamadas que o aplicativo está fazendo usando a estrutura Javavm.--aux <name=(string|bool|int)value> -usado para passar parâmetros personalizados ao gerar um aplicativo. Por exemplo --aux='uid=(int)10' gerará o aplicativo para o usuário 10 em vez do usuário padrão 0.Observação
Lembre-se de que o Frida-Server deve estar em execução antes de executar jnitrace . Se as instruções padrão para a instalação do FRIDA foram seguidas, o comando a seguir iniciará o servidor pronto para jnitrace :
adb shell /data/local/tmp/frida-server
O mecanismo que o JNitrace está disponível como um projeto separado. Esse projeto permite que você importe o JNITRACE para rastrear chamadas individuais da API JNI, em um método familiar para usar o Frida Interceptor para anexar a funções e endereços.
import { JNIInterceptor } from "jnitrace-engine" ;
JNIInterceptor . attach ( "FindClass" , {
onEnter ( args ) {
console . log ( "FindClass method called" ) ;
this . className = Memory . readCString ( args [ 1 ] ) ;
} ,
onLeave ( retval ) {
console . log ( "tLoading Class:" , this . className ) ;
console . log ( "tClass ID:" , retval . get ( ) ) ;
}
} ) ;Mais informações: https://github.com/chame1eon/jnitrace-engine
Construir jnitrace da fonte requer que node seja instalado primeiro. Depois de instalar node , os seguintes comandos precisam ser executados:
npm installnpm run watch npm run watch executará frida-compile em segundo plano compilando a fonte no arquivo de saída, build/jnitrace.js . jnitrace.py Carrega do build/jnitrace.js Por padrão, para que nenhuma outra alteração seja necessária para executar as atualizações.
Como o FRIDA-TRACE, a saída é colorida com base no thread de chamada da API.
Imediatamente abaixo do ID do thread na tela está o nome do método da API JNI. Os nomes dos métodos correspondem exatamente aos vistos no arquivo de cabeçalho jni.h
As linhas subsequentes contêm uma lista de argumentos indicados por A |- . Depois que os |- são o tipo de argumento seguido pelo valor do argumento. Para JMethods, Jfields e Jclasses, o tipo Java serão exibidos em aparelhos encaracolados. Isso depende do jnitrace ter visto o método original, o campo ou a pesquisa de classe. Para quaisquer métodos que passam buffers, jnitrace extrairá os buffers dos argumentos e o exibirá como um hexdump abaixo do valor do argumento.
Os valores de retorno são exibidos na parte inferior da lista como |= e não estarão presentes para os métodos void.
Se o backtrace estiver ativado, um backtrace FRIDA será exibido abaixo da chamada do método. Esteja ciente, de acordo com os documentos Frida, o backtrace difuso nem sempre é preciso e o backtrace preciso pode fornecer resultados limitados.
O objetivo deste projeto era criar uma ferramenta que pudesse rastrear chamadas de API JNI com eficiência para a maioria dos aplicativos Android.
Infelizmente, a abordagem mais simples de anexar a todos os ponteiros da função na estrutura JNIENV sobrecarrega o aplicativo. Isso causa uma falha com base no grande número de chamadas de função feitas por outras bibliotecas não relacionadas, também usando as mesmas funções em libart.so .
Para lidar com essa barreira de desempenho, jnitrace cria uma sombra Jnienv que pode fornecer às bibliotecas que deseja rastrear. Esse JNIENV contém uma série de trampolins de função que rejeitam as chamadas da API do JNI por meio de alguns NativeCallbacks personalizados para rastrear a entrada e saída dessas funções.
A API genérica do FRIDA faz um ótimo trabalho ao fornecer uma plataforma para construir esses trampolins de função com o mínimo de esforço. No entanto, essa abordagem simples não funciona para toda a API JNIENV. O principal problema com o rastreamento de todos os métodos é o uso de argumentos variádicos na API. Não é possível criar o NativeCallback para essas funções com antecedência, pois não é conhecido de antemão todas as diferentes combinações dos métodos Java que serão chamados.
A solução é monitorar o processo de chamadas para GetMethodID ou GetStaticMethodID , usado para procurar identificadores de método do tempo de execução. Uma vez que jnitrace vê uma pesquisa jmethodID , ele tem um mapeamento conhecido do ID para a assinatura do método. Posteriormente, quando uma chamada do método JNI Java é feita, um nativecallback inicial é usado para extrair o ID do método na chamada. Essa assinatura do método é então analisada para extrair os argumentos do método. Depois que jnitrace extraiu os argumentos no método, ele pode criar dinamicamente um nativecallback para esse método. Esse novo NativeCallback é devolvido e um pouco da arquitetura específica de shellcode lida com a configuração da pilha e se registra para permitir que essa chamada seja executada com sucesso. Esses nativecallbacks para métodos específicos são armazenados em cache para permitir que o retorno de chamada seja executado com mais eficiência, se um método, se chamado várias vezes.
O outro lugar onde um simples nativecallback não é suficiente para extrair os argumentos de uma chamada de método, é para chamadas usando um ponteiro Va_args como argumento final. Nesse caso, jnitrace usa algum código para extrair os argumentos do ponteiro fornecido. Novamente, isso é específico da arquitetura.
Todos os dados rastreados nessas chamadas de função são enviados para o aplicativo de console Python que formata e exibe -os ao usuário.
A maioria dos testes desta ferramenta foi feita em um emulador Android X86_64 executando o Marshmallow. Quaisquer problemas experimentados em execução em outro dispositivo, arquivem um problema, mas também, se possível, é recomendável tentar executar em um emulador semelhante.
Para quaisquer problemas experimentados em execução jnitrace , crie um problema no Github. Inclua as seguintes informações na questão arquivada: