18 KiB
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
-
Você trabalha em uma empresa de segurança cibernética? Você quer ver sua empresa anunciada no HackTricks? ou você quer ter acesso à última versão do PEASS ou baixar o HackTricks em PDF? Confira os PLANOS DE ASSINATURA!
-
Descubra A Família PEASS, nossa coleção exclusiva de NFTs
-
Adquira o swag oficial do PEASS & HackTricks
-
Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-me no Twitter 🐦@carlospolopm.
-
Compartilhe suas técnicas de hacking enviando PRs para o repositório hacktricks e hacktricks-cloud repo.
Informações copiadas de https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html** (você pode encontrar soluções lá)**
Os aplicativos Android podem conter bibliotecas nativas compiladas. As bibliotecas nativas são códigos que o desenvolvedor escreveu e depois compilou para uma arquitetura de computador específica. Na maioria das vezes, isso significa código escrito em C ou C++. As razões benignas, ou legítimas, pelas quais um desenvolvedor pode fazer isso é para operações matematicamente intensivas ou sensíveis ao tempo, como bibliotecas gráficas. Os desenvolvedores de malware começaram a migrar para o código nativo porque a engenharia reversa de binários compilados tende a ser um conjunto de habilidades menos comum do que a análise do bytecode DEX. Isso se deve em grande parte ao fato de que o bytecode DEX pode ser descompilado para Java, enquanto o código nativo compilado geralmente deve ser analisado como assembly.
Objetivo
O objetivo desta seção não é ensinar assembly (ASM) ou como engenharia reversa de código compilado de forma mais geral, mas sim como aplicar as habilidades de engenharia reversa binária mais gerais, especificamente para Android. Como o objetivo deste workshop não é ensinar as arquiteturas ASM, todos os exercícios incluirão uma versão ARM e uma versão x86 da biblioteca a ser analisada para que cada pessoa possa escolher a arquitetura com a qual se sinta mais confortável.
Aprendendo a Assembleia ARM
Se você não tem experiência anterior em engenharia reversa binária / assembly, aqui estão alguns recursos sugeridos. A maioria dos dispositivos Android roda em ARM, mas todos os exercícios deste workshop também incluem uma versão x86 da biblioteca.
Para aprender e / ou revisar a assembleia ARM, eu sugiro fortemente o ARM Assembly Basics do Azeria Labs.
Introdução à Interface Nativa do Java (JNI)
A Interface Nativa do Java (JNI) permite que os desenvolvedores declarem métodos Java que são implementados em código nativo (geralmente compilado em C/C++). A interface JNI não é específica do Android, mas está disponível mais geralmente para aplicativos Java que são executados em diferentes plataformas.
O Android Native Development Kit (NDK) é o conjunto de ferramentas específico do Android em cima do JNI. De acordo com a documentação:
No Android, o Kit de Desenvolvimento Nativo (NDK) é um conjunto de ferramentas que permite que os desenvolvedores escrevam código C e C++ para seus aplicativos Android.
Juntos, JNI e NDK permitem que os desenvolvedores Android implementem parte da funcionalidade de seus aplicativos em código nativo. O código Java (ou Kotlin) chamará um método nativo declarado em Java que é implementado na biblioteca nativa compilada.
Referências
Documentação Oracle JNI
- Especificação JNI
- Funções JNI <– Eu sempre tenho este aberto e me refiro a ele ao reverter bibliotecas nativas do Android
Referências do JNI e NDK do Android
- Dicas do JNI do Android <– Altamente sugiro ler a seção "Bibliotecas nativas" para começar
- Introdução ao NDK <– Este é um guia para como os desenvolvedores desenvolvem bibliotecas nativas e entender como as coisas são construídas, torna mais fácil reverter.
Alvo de Análise - Bibliotecas Nativas do Android
Para esta seção, estamos nos concentrando em como reverter a funcionalidade do aplicativo que foi implementada em bibliotecas nativas do Android. Quando dizemos bibliotecas nativas do Android, o que queremos dizer?
As bibliotecas nativas do Android são incluídas nos APKs como bibliotecas de objeto compartilhado .so
, no formato de arquivo ELF. Se você já analisou binários Linux anteriormente, é o mesmo formato.
Essas bibliotecas por padrão são incluídas no APK no caminho do arquivo /lib/<cpu>/lib<name>.so
. Este é o caminho padrão, mas os desenvolvedores também podem optar por incluir a biblioteca nativa em /assets/<custom_name>
se assim o desejarem. Com mais frequência, estamos vendo desenvolvedores de malware escolherem incluir bibliotecas nativas em caminhos diferentes de /lib
e usando diferentes extensões de arquivo para tentar "ocultar" a presença da biblioteca nativa.
Como o código nativo é compilado para CPUs específicas, se um desenvolvedor quiser que seu aplicativo seja executado em mais de um tipo de hardware, ele terá que incluir cada uma dessas versões da biblioteca nativa compilada no aplicativo. O caminho padrão mencionado acima inclui um diretório para cada tipo de CPU oficialmente suportado pelo Android.
CPU | Caminho da Biblioteca Nativa |
---|---|
“generic” 32-bit ARM | lib/armeabi/libcalc.so |
x86 | lib/x86/libcalc.so |
x64 | lib/x86_64/libcalc.so |
ARMv7 | lib/armeabi-v7a/libcalc.so |
ARM64 | lib/arm64-v8a/libcalc.so |
Carregando a Biblioteca
Antes que um aplicativo Android possa chamar e executar qualquer código que seja implementado em uma biblioteca nativa, o aplicativo (código Java) deve carregar a biblioteca na memória. Existem duas chamadas de API diferentes que farão isso:
System.loadLibrary("calc")
Desculpe, eu não entendi. Você poderia reformular sua solicitação?
System.load("lib/armeabi/libcalc.so")
A diferença entre as duas chamadas de API é que loadLibrary
só recebe o nome curto da biblioteca como argumento (ou seja, libcalc.so = "calc" e libinit.so = "init") e o sistema determinará corretamente a arquitetura em que está sendo executado e, portanto, o arquivo correto a ser usado. Por outro lado, load
requer o caminho completo para a biblioteca. Isso significa que o desenvolvedor do aplicativo tem que determinar a arquitetura e, portanto, o arquivo de biblioteca correto a ser carregado.
Quando qualquer um desses dois APIs (loadLibrary
ou load
) é chamado pelo código Java, a biblioteca nativa que é passada como argumento executa seu JNI_OnLoad
se ele foi implementado na biblioteca nativa.
Para reiterar, antes de executar quaisquer métodos nativos, a biblioteca nativa deve ser carregada chamando System.loadLibrary
ou System.load
no código Java. Quando qualquer um desses 2 APIs é executado, a função JNI_OnLoad
na biblioteca nativa também é executada.
A Conexão de Código Java para Código Nativo
Para executar uma função da biblioteca nativa, deve haver um método nativo declarado em Java que o código Java possa chamar. Quando este método nativo declarado em Java é chamado, a função nativa "pareada" da biblioteca nativa (ELF/.so) é executada.
Um método nativo declarado em Java aparece no código Java como abaixo. Ele aparece como qualquer outro método Java, exceto que inclui a palavra-chave native
e não tem código em sua implementação, porque seu código está realmente na biblioteca nativa compilada.
public native String doThingsInNativeLibrary(int var0);
Para chamar este método nativo, o código Java o chamaria como qualquer outro método Java. No entanto, nos bastidores, o JNI e o NDK executariam a função correspondente na biblioteca nativa. Para isso, é necessário conhecer a associação entre um método nativo declarado em Java e uma função na biblioteca nativa.
Existem duas maneiras diferentes de fazer essa associação, ou vinculação:
- Vinculação dinâmica usando a Resolução de Nome de Método Nativo JNI, ou
- Vinculação estática usando a chamada de API
RegisterNatives
Vinculação Dinâmica
Para vincular, ou associar, o método nativo declarado em Java e a função na biblioteca nativa dinamicamente, o desenvolvedor nomeia o método e a função de acordo com as especificações para que o sistema JNI possa fazer a vinculação dinamicamente.
De acordo com a especificação, o desenvolvedor nomearia a função da seguinte forma para que o sistema possa vincular dinamicamente o método nativo e a função. Um nome de método nativo é concatenado a partir dos seguintes componentes:
- o prefixo Java_
- um nome de classe qualificado completo codificado
- um separador de sublinhado (“_”)
- um nome de método codificado
- para métodos nativos sobrecarregados, dois sublinhados (“__”) seguidos da assinatura de argumento codificada
Para fazer a vinculação dinâmica para o método nativo declarado em Java abaixo e digamos que ele esteja na classe com.android.interesting.Stuff
public native String doThingsInNativeLibrary(int var0);
A função na biblioteca nativa precisaria ter o nome:
Java_com_android_interesting_Stuff_doThingsInNativeLibrary
Se não houver uma função na biblioteca nativa com esse nome, isso significa que o aplicativo deve estar fazendo uma vinculação estática.
Vinculação Estática
Se o desenvolvedor não quiser ou não puder nomear as funções nativas de acordo com a especificação (por exemplo, deseja remover símbolos de depuração), ele deve usar a vinculação estática com a API RegisterNatives
(doc) para fazer a associação entre o método nativo declarado em Java e a função na biblioteca nativa. A função RegisterNatives
é chamada a partir do código nativo, não do código Java, e é mais frequentemente chamada na função JNI_OnLoad
, uma vez que RegisterNatives
deve ser executado antes de chamar o método nativo declarado em Java.
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
Ao realizar engenharia reversa, se a aplicação estiver usando o método de vinculação estática, nós, como analistas, podemos encontrar a estrutura JNINativeMethod
que está sendo passada para RegisterNatives
para determinar qual sub-rotina na biblioteca nativa é executada quando o método nativo declarado em Java é chamado.
A estrutura JNINativeMethod
requer uma string com o nome do método nativo declarado em Java e uma string com a assinatura do método, então devemos ser capazes de encontrar essas informações em nossa biblioteca nativa.
Assinatura do Método
A estrutura JNINativeMethod
requer a assinatura do método. Uma assinatura do método indica os tipos dos argumentos que o método recebe e o tipo do que ele retorna. Este link documenta Assinaturas de Tipo JNI na seção "Assinaturas de Tipo".
- Z: booleano
- B: byte
- C: char
- S: short
- I: int
- J: long
- F: float
- D: double
- L fully-qualified-class ; :classe total-qualificada
- [ type: type[]: tipo de matriz
- ( arg-types ) ret-type: tipo de método
- V: vazio
Para o método nativo.
public native String doThingsInNativeLibrary(int var0);
A assinatura de tipo é
(I)Ljava/lang/String;
Aqui está outro exemplo de um método nativo e sua assinatura. Para o seguinte é a declaração do método:
public native long f (int n, String s, int[] arr);
Possui a assinatura de tipo:
(ILjava/lang/String;[I)J
Exercício #5 - Encontre o endereço da função nativa
No Exercício #5, vamos aprender a carregar bibliotecas nativas em um desmontador e identificar a função nativa que é executada quando um método nativo é chamado. Para este exercício em particular, o objetivo não é engenharia reversa do método nativo, apenas encontrar o link entre a chamada ao método nativo em Java e a função que é executada na biblioteca nativa. Para este exercício, usaremos o aplicativo de amostra Mediacode.apk. Esta amostra está disponível em ~/samples/Mediacode.apk
na VM. Seu hash SHA256 é a496b36cda66aaf24340941da8034bd53940d1b08d83a97f17a65ae144ebf91a.
Objetivo
O objetivo deste exercício é:
- Identificar métodos nativos declarados no bytecode DEX
- Determinar quais bibliotecas nativas são carregadas (e, portanto, onde os métodos nativos podem ser implementados)
- Extrair a biblioteca nativa do APK
- Carregar a biblioteca nativa em um desmontador
- Identificar o endereço (ou nome) da função na biblioteca nativa que é executada quando o método nativo é chamado
Instruções
- Abra o Mediacode.apk no jadx. Consulte o Exercício #1 para obter mais informações.
- Desta vez, se você expandir a guia Recursos, verá que este APK tem um diretório
lib/
. As bibliotecas nativas para este APK estão nos caminhos padrão da CPU. - Agora precisamos identificar quaisquer métodos nativos declarados. No jadx, pesquise e liste todos os métodos nativos declarados. Deve haver dois.
- Em torno do método nativo declarado, veja se há algum lugar em que uma biblioteca nativa é carregada. Isso fornecerá orientação sobre em qual biblioteca nativa procurar a função a ser implementada.
- Extraia a biblioteca nativa do APK criando um novo diretório e copiando o APK para essa pasta. Em seguida, execute o comando
unzip Mediacode.APK
. Você verá todos os arquivos extraídos do APK, que incluem o diretóriolib/
. - Selecione a arquitetura da biblioteca nativa que você deseja analisar.
- Inicie o ghidra executando
ghidraRun
. Isso abrirá o Ghidra. - Para abrir a biblioteca nativa para análise, selecione "Novo Projeto", "Projeto Não Compartilhado", selecione um caminho para salvar o projeto e dê um nome a ele. Isso cria um projeto que você pode então carregar arquivos binários.
- Depois de criar seu projeto, selecione o ícone do dragão para abrir o Navegador de Código. Vá em "Arquivo" > "Importar Arquivo" para carregar a biblioteca nativa na ferramenta. Você pode deixar todos os padrões.
- Você verá a seguinte tela. Selecione "Analisar".
- Usando as informações de vinculação acima, identifique a função na biblioteca nativa que é executada quando o método nativo declarado em Java é chamado.
Solução
Reversão de código de bibliotecas nativas do Android - JNIEnv
Ao começar a engenharia reversa de bibliotecas nativas do Android, uma das coisas que eu não sabia que precisava saber era sobre JNIEnv
. JNIEnv
é uma estrutura de ponteiros de função para Funções JNI. Toda função JNI em bibliotecas nativas do Android, leva JNIEnv*
como primeiro argumento.
Da documentação do Android JNI Tips:
As declarações C de JNIEnv e JavaVM são diferentes das declarações C++. O arquivo de inclusão "jni.h" fornece diferentes typedefs dependendo se ele é incluído em C ou C++. Por esse motivo, é uma má ideia incluir argumentos JNIEnv em arquivos de cabeçalho incluídos por ambos os idiomas. (Dito de outra forma: se o seu arquivo de cabeçalho requer #ifdef __cplusplus, você pode ter que fazer algum trabalho extra se algo nesse cabeçalho se referir a JNIEnv.)
Aqui estão algumas funções