MASTG-TECH-0018: Desmontagem de Native Code
Tanto o Dalvik quanto o ART suportam a Java Native Interface (JNI), que define uma forma de o código Java interagir com código nativo escrito em C/C++. Como em outros sistemas operacionais baseados em Linux, o código nativo é empacotado (compilado) em bibliotecas dinâmicas ELF (*.so), que o aplicativo Android carrega durante a execução por meio do método System.load. No entanto, em vez de depender de bibliotecas C amplamente utilizadas (como glibc), os binários do Android são construídos em relação a uma libc personalizada chamada Bionic. A Bionic adiciona suporte a serviços importantes específicos do Android, como propriedades do sistema e registro de logs, e não é totalmente compatível com POSIX.
Ao fazer a reversão de um aplicativo Android que contém código nativo, precisamos entender algumas estruturas de dados relacionadas à ponte JNI entre o código Java e o código nativo. Do ponto de vista da reversão, precisamos estar cientes de duas estruturas de dados principais: JavaVM e JNIEnv. Ambas são ponteiros para ponteiros de tabelas de funções:
JavaVMfornece uma interface para invocar funções para criar e destruir uma JavaVM. O Android permite apenas umaJavaVMpor processo e não é realmente relevante para nossos propósitos de reversão.JNIEnvfornece acesso à maioria das funções JNI, que são acessíveis em um deslocamento fixo por meio do ponteiroJNIEnv. Este ponteiroJNIEnvé o primeiro parâmetro passado para cada função JNI. Discutiremos este conceito novamente com a ajuda de um exemplo mais adiante neste capítulo.
Vale destacar que analisar código nativo desmontado é muito mais desafiador do que código Java desmontado. Ao fazer a reversão do código nativo em um aplicativo Android, precisaremos de um desmontador.
No próximo exemplo, faremos a reversão do HelloWorld-JNI.apk do repositório OWASP MASTG. Instalá-lo e executá-lo em um emulador ou dispositivo Android é opcional.
wget https://github.com/OWASP/mastg/raw/master/Samples/Android/01_HelloWorld-JNI/HelloWord-JNI.apk
Este aplicativo não é exatamente espetacular; tudo o que ele faz é mostrar um rótulo com o texto "Hello from C++". Este é o aplicativo que o Android gera por padrão quando você cria um novo projeto com suporte a C/C++, que é suficiente para mostrar os princípios básicos das chamadas JNI.

Descompile o APK com apkx.
$ apkx HelloWord-JNI.apk
Extracting HelloWord-JNI.apk to HelloWord-JNI
Converting: classes.dex -> classes.jar (dex2jar)
dex2jar HelloWord-JNI/classes.dex -> HelloWord-JNI/classes.jar
Decompiling to HelloWord-JNI/src (cfr)
Isso extrai o código-fonte para o diretório HelloWord-JNI/src. A atividade principal é encontrada no arquivo HelloWord-JNI/src/sg/vantagepoint/helloworldjni/MainActivity.java. A TextView "Hello World" é preenchida no método onCreate:
public class MainActivity
extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
this.setContentView(2130968603);
((TextView)this.findViewById(2131427422)).setText((CharSequence)this. \
stringFromJNI());
}
public native String stringFromJNI();
}
Observe a declaração de public native String stringFromJNI na parte inferior. A palavra-chave "native" informa ao compilador Java que este método é implementado em uma linguagem nativa. A função correspondente é resolvida durante a execução, mas somente se uma biblioteca nativa que exporta um símbolo global com a assinatura esperada for carregada (as assinaturas compreendem um nome de pacote, nome de classe e nome do método). Neste exemplo, esse requisito é satisfeito pela seguinte função em C ou C++:
JNIEXPORT jstring JNICALL Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI(JNIEnv *env, jobject)
Então, onde está a implementação nativa desta função? Se você olhar no diretório "lib" do arquivo APK descompactado, verá vários subdiretórios (um por arquitetura de processador suportada), cada um contendo uma versão da biblioteca nativa, neste caso libnative-lib.so. Quando System.loadLibrary é chamado, o carregador seleciona a versão correta com base no dispositivo em que o aplicativo está sendo executado. Antes de prosseguir, preste atenção no primeiro parâmetro passado para a função JNI atual. É a mesma estrutura de dados JNIEnv discutida anteriormente nesta seção.

Seguindo a convenção de nomenclatura mencionada acima, você pode esperar que a biblioteca exporte um símbolo chamado Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI. Em sistemas Linux, você pode recuperar a lista de símbolos com readelf (incluído no GNU binutils) ou nm. Faça isso no macOS com a ferramenta greadelf, que você pode instalar via Macports ou Homebrew. O exemplo a seguir usa greadelf:
$ greadelf -W -s libnative-lib.so | grep Java
3: 00004e49 112 FUNC GLOBAL DEFAULT 11 Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI
Você também pode ver isso usando rabin2 do radare2:
$ rabin2 -s HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so | grep -i Java
003 0x00000e78 0x00000e78 GLOBAL FUNC 16 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
Esta é a função nativa que eventualmente é executada quando o método nativo stringFromJNI é chamado.
Para desmontar o código, você pode carregar libnative-lib.so em qualquer desmontador que entenda binários ELF (ou seja, qualquer desmontador). Se o aplicativo incluir binários para diferentes arquiteturas, você pode teoricamente escolher a arquitetura com a qual está mais familiarizado, desde que seja compatível com o desmontador. Cada versão é compilada a partir do mesmo código-fonte e implementa a mesma funcionalidade. No entanto, se você planeja depurar a biblioteca em um dispositivo real posteriormente, geralmente é aconselhável escolher uma compilação ARM.
Para suportar processadores ARM mais antigos e mais recentes, os aplicativos Android incluem várias compilações ARM compiladas para diferentes versões da Application Binary Interface (ABI). A ABI define como o código de máquina do aplicativo deve interagir com o sistema durante a execução. As seguintes ABIs são suportadas:
- armeabi: ABI para CPUs baseadas em ARM que suportam pelo menos o conjunto de instruções ARMv5TE.
- armeabi-v7a: Esta ABI estende a armeabi para incluir várias extensões do conjunto de instruções da CPU.
- arm64-v8a: ABI para CPUs baseadas em ARMv8 que suportam AArch64, a nova arquitetura ARM de 64 bits.
A maioria dos desmontadores pode lidar com qualquer uma dessas arquiteturas. A seguir, visualizaremos a versão armeabi-v7a (localizada em HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so) no radare2 e no IDA Pro. Consulte Análise de Código Nativo Desmontado para aprender como proceder ao inspecionar o código nativo desmontado.
radare2¶
Para abrir o arquivo no radare2 para Android, você só precisa executar r2 -A HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so. O capítulo "Teste Básico de Segurança Android)" já introduziu o radare2. Lembre-se de que você pode usar a flag -A para executar o comando aaa logo após carregar o binário para analisar todo o código referenciado.
$ r2 -A HelloWord-JNI/lib/armeabi-v7a/libnative-lib.so
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Finding xrefs in noncode section with anal.in=io.maps
[x] Analyze value pointers (aav)
[x] Value from 0x00000000 to 0x00001dcf (aav)
[x] 0x00000000-0x00001dcf in 0x0-0x1dcf (aav)
[x] Emulate code to find computed references (aae)
[x] Type matching analysis for all functions (aaft)
[x] Use -AA or aaaa to perform additional experimental analysis.
-- Print the contents of the current block with the 'p' command
[0x00000e3c]>
Observe que, para binários maiores, iniciar diretamente com a flag -A pode ser muito demorado e desnecessário. Dependendo do seu propósito, você pode abrir o binário sem esta opção e então aplicar uma análise menos complexa como aa ou um tipo mais concreto de análise, como os oferecidos em aa (análise básica de todas as funções) ou aac (analisar chamadas de função). Lembre-se de sempre digitar ? para obter ajuda ou anexá-lo a comandos para ver ainda mais comandos ou opções. Por exemplo, se você digitar aa?, obterá a lista completa de comandos de análise.
[0x00001760]> aa?
Usage: aa[0*?] # see also 'af' and 'afna'
| aa alias for 'af@@ sym.*;af@entry0;afva'
| aaa[?] autoname functions after aa (see afna)
| aab abb across bin.sections.rx
| aac [len] analyze function calls (af @@ `pi len~call[1]`)
| aac* [len] flag function calls without performing a complete analysis
| aad [len] analyze data references to code
| aae [len] ([addr]) analyze references with ESIL (optionally to address)
| aaf[e|t] analyze all functions (e anal.hasnext=1;afr @@c:isq) (aafe=aef@@f)
| aaF [sym*] set anal.in=block for all the spaces between flags matching glob
| aaFa [sym*] same as aaF but uses af/a2f instead of af+/afb+ (slower but more accurate)
| aai[j] show info of all analysis parameters
| aan autoname functions that either start with fcn.* or sym.func.*
| aang find function and symbol names from golang binaries
| aao analyze all objc references
| aap find and analyze function preludes
| aar[?] [len] analyze len bytes of instructions for references
| aas [len] analyze symbols (af @@= `isq~[0]`)
| aaS analyze all flags starting with sym. (af @@ sym.*)
| aat [len] analyze all consecutive functions in section
| aaT [len] analyze code after trap-sleds
| aau [len] list mem areas (larger than len bytes) not covered by functions
| aav [sat] find values referencing a specific section or map
Há uma coisa que vale a pena notar sobre o radare2 em comparação com outros desmontadores como, por exemplo, o IDA Pro. A seguinte citação deste artigo do blog do radare2 (https://radareorg.github.io/blog/) oferece um bom resumo.
A análise de código não é uma operação rápida, e nem mesmo previsível ou que leve um tempo linear para ser processada. Isso torna os tempos de inicialização bastante pesados, em comparação com apenas carregar os cabeçalhos e informações de strings como é feito por padrão.
Pessoas acostumadas com o IDA Pro ou Hopper apenas carregam o binário, saem para tomar um café e então, quando a análise é concluída, começam a fazer a análise manual para entender o que o programa está fazendo. É verdade que essas ferramentas executam a análise em segundo plano, e a GUI não fica bloqueada. Mas isso consome muito tempo de CPU, e o r2 visa executar em muito mais plataformas do que apenas computadores desktop de alto desempenho.
Dito isso, consulte Análise de Código Nativo Desmontado para saber mais sobre como o radare2 pode nos ajudar a realizar nossas tarefas de reversão muito mais rapidamente. Por exemplo, obter a desmontagem de uma função específica é uma tarefa trivial que pode ser realizada em um comando.
IDA Pro¶
Se você possui uma licença do IDA Pro, abra o arquivo e, uma vez no diálogo "Load new file", escolha "ELF for ARM (Shared Object)" como o tipo de arquivo (o IDA deve detectar isso automaticamente) e "ARM Little-Endian" como o tipo de processador.

A versão gratuita do IDA Pro, infelizmente, não suporta o tipo de processador ARM.