Engenharia Reversa de Bibliotecas Nativas

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Para mais informações, consulte: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Aplicativos Android podem usar bibliotecas nativas, tipicamente escritas em C ou C++, para tarefas críticas de desempenho. Criadores de malware também abusam dessas bibliotecas porque ELF shared objects ainda são mais difíceis de decompilar do que byte-code DEX/OAT. Esta página foca em fluxos de trabalho práticos e melhorias de ferramentas recentes (2023-2025) que tornam a engenharia reversa de arquivos .so do Android mais fácil.


Fluxo rápido de triagem para um libfoo.so recém-extraído

  1. Extrair a biblioteca
# From an installed application
adb shell "run-as <pkg> cat lib/arm64-v8a/libfoo.so" > libfoo.so
# Or from the APK (zip)
unzip -j target.apk "lib/*/libfoo.so" -d extracted_libs/
  1. Identificar arquitetura e proteções
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. Listar símbolos exportados e bindings JNI
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. Carregar em um decompilador (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper ou Cutter/Rizin) e executar a análise automática. Versões mais recentes do Ghidra introduziram um decompilador AArch64 que reconhece PAC/BTI stubs e tags MTE, melhorando muito a análise de bibliotecas compiladas com o Android 14 NDK.
  2. Decidir entre engenharia reversa estática vs. dinâmica: código stripped, obfuscated frequentemente precisa de instrumentation (Frida, ptrace/gdbserver, LLDB).

Instrumentação Dinâmica (Frida ≥ 16)

A série 16 do Frida trouxe várias melhorias específicas para Android que ajudam quando o alvo usa otimizações modernas do Clang/LLD:

  • thumb-relocator agora pode hook funções ARM/Thumb minúsculas geradas pelo alinhamento agressivo do LLD (--icf=all).
  • A enumeração e reatribuição de ELF import slots funciona no Android, permitindo patching por módulo com dlopen()/dlsym() quando inline hooks são rejeitados.
  • O Java hooking foi corrigido para o novo ART quick-entrypoint usado quando apps são compilados com --enable-optimizations no Android 14.

Exemplo: enumerar todas as funções registradas via RegisterNatives e imprimir seus endereços em tempo de execução:

Java.perform(function () {
var Runtime = Java.use('java.lang.Runtime');
var register = Module.findExportByName(null, 'RegisterNatives');
Interceptor.attach(register, {
onEnter(args) {
var envPtr  = args[0];
var clazz   = Java.cast(args[1], Java.use('java.lang.Class'));
var methods = args[2];
var count   = args[3].toInt32();
console.log('[+] RegisterNatives on ' + clazz.getName() + ' -> ' + count + ' methods');
// iterate & dump (JNI nativeMethod struct: name, sig, fnPtr)
}
});
});

Frida funcionará imediatamente em dispositivos com PAC/BTI (Pixel 8/Android 14+) desde que você utilize frida-server 16.2 ou posterior – versões anteriores não conseguiam localizar o padding para inline hooks.

Dumping runtime-decrypted native libraries from memory (Frida soSaver)

Quando um APK protegido mantém código nativo criptografado ou o mapeia apenas em tempo de execução (packers, downloaded payloads, generated libs), anexe o Frida e extraia o ELF mapeado diretamente da memória do processo.

soSaver workflow (Python host + TS/JS Frida agent):

  • Intercepta dlopen e android_dlopen_ext para detectar o mapeamento de bibliotecas no momento do carregamento e realiza uma varredura inicial dos módulos já carregados.
  • Periodicamente varre os mapeamentos de memória do processo procurando por cabeçalhos ELF para capturar módulos carregados por mapeadores não padrão que nunca passam pelas loader APIs.
  • Lê cada módulo em blocos da memória e transmite os bytes via mensagens do Frida para o host; se uma região não puder ser lida, recorre à leitura do caminho no disco quando disponível.
  • Salva os arquivos .so reconstruídos e imprime estatísticas de extração por módulo, fornecendo artefatos para static RE.

Run (root + frida-server, Python ≥3.8, uv):

git clone https://github.com/TheQmaks/sosaver.git
cd sosaver && uv sync
source .venv/bin/activate    # .venv\Scripts\activate on Windows

# target by package or PID; choose output/verbosity
sosaver com.example.app
sosaver 1234 -o /tmp/so-dumps --debug

Essa abordagem contorna proteções “only decrypted in RAM” recuperando a imagem mapeada em tempo de execução, permitindo análise offline em IDA/Ghidra mesmo se a cópia no sistema de arquivos estiver ofuscada ou ausente.

Process-local JNI telemetry via preloaded .so (SoTap)

Quando instrumentação completa é exagerada ou bloqueada, você ainda pode obter visibilidade a nível nativo pré-carregando um pequeno logger dentro do processo alvo. SoTap é uma biblioteca nativa Android leve (.so) que registra o comportamento em tempo de execução de outras bibliotecas JNI (.so) dentro do mesmo processo do app (não requer root).

Key properties:

  • Inicializa cedo e observa interações JNI/native dentro do processo que o carrega.
  • Persiste logs usando múltiplos caminhos graváveis com fallback elegante para Logcat quando o armazenamento é restrito.
  • Source-customizable: edite sotap.c para estender/ajustar o que é registrado e reconstrua por ABI.

Setup (repack the APK):

  1. Drop the proper ABI build into the APK so the loader can resolve libsotap.so:
  • lib/arm64-v8a/libsotap.so (for arm64)
  • lib/armeabi-v7a/libsotap.so (for arm32)
  1. Ensure SoTap loads before other JNI libs. Inject a call early (e.g., Application subclass static initializer or onCreate) so the logger is initialized first. Smali snippet example:
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  1. Rebuild/sign/install, run the app, then collect logs.

Log paths (checked in order):

/data/user/0/%s/files/sotap.log
/data/data/%s/files/sotap.log
/sdcard/Android/data/%s/files/sotap.log
/sdcard/Download/sotap-%s.log
# If all fail: fallback to Logcat only

Notas e resolução de problemas:

  • ABI alignment é obrigatório. Um descompasso causará UnsatisfiedLinkError e o logger não será carregado.
  • Restrições de armazenamento são comuns em Android modernos; se gravações em arquivo falharem, o SoTap ainda emitirá via Logcat.
  • Comportamento/verbosidade deve ser personalizado; reconstrua a partir do source após editar sotap.c.

Esta abordagem é útil para malware triage e debugging de JNI quando observar fluxos de chamada nativos desde o início do processo é crítico, mas hooks em root/sistema não estão disponíveis.


Veja também: execução de código nativo em memória via JNI

Um padrão de ataque comum é baixar um blob de shellcode cru em runtime e executá‑lo diretamente da memória através de uma ponte JNI (sem ELF em disco). Detalhes e snippet JNI pronto para uso aqui:

In Memory Jni Shellcode Execution


Vulnerabilidades recentes que valem caçar em APKs

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1Heap buffer overflow alcançável a partir de código nativo que decodifica imagens WebP. Vários apps Android embutem versões vulneráveis. Quando você vê um libwebp.so dentro de um APK, verifique sua versão e tente exploração ou patch.
2024MultipleOpenSSL 3.x seriesVárias issues de segurança de memória e padding-oracle. Muitos bundles Flutter & ReactNative entregam seu próprio libcrypto.so.

Quando encontrar arquivos .so de third-party dentro de um APK, sempre compare seu hash com advisories upstream. SCA (Software Composition Analysis) é incomum em mobile, então builds desatualizados e vulneráveis são comuns.


Tendências de Anti‑Reversing & Hardening (Android 13-15)

  • Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 habilita PAC/BTI em system libraries em silicon ARMv8.3+ suportado. Decompilers agora mostram pseudo‑instruções relacionadas a PAC; para análise dinâmica, o Frida injeta trampolines após remover PAC, mas seus trampolines customizados devem chamar pacda/autibsp quando necessário.
  • MTE & Scudo hardened allocator: memory-tagging é opt‑in, mas muitos apps com Play‑Integrity constroem com -fsanitize=memtag; use setprop arm64.memtag.dump 1 mais adb shell am start ... para capturar tag faults.
  • LLVM Obfuscator (opaque predicates, control-flow flattening): packers comerciais (e.g., Bangcle, SecNeo) protegem cada vez mais código nativo, não apenas Java; espere control-flow falso e blobs de strings encriptadas em .rodata.

Neutralizando inicializadores nativos precoces (.init_array) e JNI_OnLoad para instrumentação precoce (ARM64 ELF)

Apps altamente protegidos frequentemente colocam checks de root/emulator/debug em construtores nativos que rodam muito cedo via .init_array, antes de JNI_OnLoad e muito antes de qualquer código Java executar. Você pode tornar esses inicializadores implícitos explícitos e recuperar controle por meio de:

  • Remover INIT_ARRAY/INIT_ARRAYSZ da tabela DYNAMIC para que o loader não auto‑execute entradas de .init_array.
  • Resolver o endereço do construtor a partir de relocations RELATIVE e exportá‑lo como um símbolo de função regular (ex.: INIT0).
  • Renomear JNI_OnLoad para JNI_OnLoad0 para evitar que o ART o chame implicitamente.

Porque isso funciona no Android/arm64

  • Em AArch64, entradas de .init_array frequentemente são populadas em tempo de load por relocations R_AARCH64_RELATIVE cujo addend é o endereço da função alvo dentro de .text.
  • Os bytes de .init_array podem parecer vazios estaticamente; o dynamic linker escreve o endereço resolvido durante o processamento de relocations.

Identificando o alvo do construtor

  • Use o Android NDK toolchain para parsing preciso de ELF em AArch64:
# Adjust paths to your NDK; use the aarch64-linux-android-* variants
readelf -W -a ./libnativestaticinit.so | grep -n "INIT_ARRAY" -C 4
readelf -W --relocs ./libnativestaticinit.so
  • Encontre a relocation que cai dentro do range de endereço virtual de .init_array; o addend desse R_AARCH64_RELATIVE é o construtor (ex.: 0xA34, 0x954).
  • Desassemble ao redor desse endereço para checar sanidade:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Plano de patch

  1. Remover as tags DYNAMIC INIT_ARRAY e INIT_ARRAYSZ. Não deletar sections.
  2. Adicionar um símbolo GLOBAL DEFAULT FUNC INIT0 no endereço do construtor para que possa ser chamado manualmente.
  3. Renomear JNI_OnLoadJNI_OnLoad0 para impedir que o ART o invoque implicitamente.

Validação após o patch

readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'

Aplicando patches com LIEF (Python)

Script: remover INIT_ARRAY/INIT_ARRAYSZ, exportar INIT0, renomear JNI_OnLoad→JNI_OnLoad0 ```python import lief

b = lief.parse(“libnativestaticinit.so”)

Locate .init_array VA range

init = b.get_section(‘.init_array’) va, sz = init.virtual_address, init.size

Compute constructor address from RELATIVE relocation landing in .init_array

ctor = None for r in b.dynamic_relocations: if va <= r.address < va + sz: ctor = r.addend break if ctor is None: raise RuntimeError(“No R_*_RELATIVE relocation found inside .init_array”)

Remove auto-run tags so loader skips .init_array

for tag in (lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ, lief.ELF.DYNAMIC_TAGS.INIT_ARRAY): try: b.remove(b[tag]) except Exception: pass

Add exported FUNC symbol INIT0 at constructor address

sym = lief.ELF.Symbol() sym.name = ‘INIT0’ sym.value = ctor sym.size = 0 sym.binding = lief.ELF.SYMBOL_BINDINGS.GLOBAL sym.type = lief.ELF.SYMBOL_TYPES.FUNC sym.visibility = lief.ELF.SYMBOL_VISIBILITY.DEFAULT

Place symbol in .text index

text = b.get_section(‘.text’) for idx, sec in enumerate(b.sections): if sec == text: sym.shndx = idx break b.add_dynamic_symbol(sym)

Rename JNI_OnLoad -> JNI_OnLoad0 to block implicit ART init

j = b.get_symbol(‘JNI_OnLoad’) if j: j.name = ‘JNI_OnLoad0’

b.write(‘libnativestaticinit.so.patched’)

</details>

Notas e abordagens que falharam (para portabilidade)
- Zerar bytes de `.init_array` ou definir o comprimento da seção como 0 não ajuda: o linker dinâmico repopula-a via relocations.
- Ajustar `INIT_ARRAY`/`INIT_ARRAYSZ` para 0 pode quebrar o loader devido a tags inconsistentes. A remoção limpa dessas entradas DYNAMIC é a alavanca confiável.
- Excluir completamente a seção `.init_array` tende a travar o loader.
- Depois de aplicar o patch, os endereços de função/layout podem mudar; sempre recompute o construtor a partir dos addends de `.rela.dyn` no arquivo patchado se precisar reexecutar o patch.

Inicializando um ART/JNI mínimo para invocar INIT0 e JNI_OnLoad0
- Use JNIInvocation para subir um pequeno contexto ART VM em um binário standalone. Então chame `INIT0()` e `JNI_OnLoad0(vm)` manualmente antes de qualquer código Java.
- Inclua o APK/classes alvo no classpath para que qualquer `RegisterNatives` encontre suas classes Java.

<details>
<summary>Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method</summary>
```cmake
# CMakeLists.txt
project(caller)
cmake_minimum_required(VERSION 3.8)
include_directories(AFTER ${CMAKE_SOURCE_DIR}/include)
link_directories(${CMAKE_SOURCE_DIR}/lib)
find_library(log-lib log REQUIRED)
add_executable(caller "caller.c")
add_library(jenv SHARED "jnihelper.c")
target_link_libraries(caller jenv nativestaticinit)
// caller.c
#include <jni.h>
#include "jenv.h"
JavaCTX ctx;
void INIT0();
void JNI_OnLoad0(JavaVM* vm);
int main(){
char *jvmopt = "-Djava.class.path=/data/local/tmp/base.apk"; // include app classes
if (initialize_java_environment(&ctx,&jvmopt,1)!=0) return -1;
INIT0();                   // manual constructor
JNI_OnLoad0(ctx.vm);       // manual JNI init
jclass c = (*ctx.env)->FindClass(ctx.env, "eu/nviso/nativestaticinit/MainActivity");
jmethodID m = (*ctx.env)->GetStaticMethodID(ctx.env,c,"stringFromJNI","()Ljava/lang/String;");
jstring s = (jstring)(*ctx.env)->CallStaticObjectMethod(ctx.env,c,m);
const char* p = (*ctx.env)->GetStringUTFChars(ctx.env,s,NULL);
printf("Native string: %s\n", p);
cleanup_java_env(&ctx);
}
# Build (adjust NDK/ABI)
cmake -DANDROID_PLATFORM=31 \
-DCMAKE_TOOLCHAIN_FILE=$HOME/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a ..
make

Problemas Comuns:

  • Endereços de construtores mudam após patching devido ao re-layout; sempre recalcule a partir de .rela.dyn no binário final.
  • Garanta que -Djava.class.path cubra todas as classes usadas pelas chamadas RegisterNatives.
  • O comportamento pode variar com versões do NDK/loader; a etapa consistentemente confiável foi remover as tags DYNAMIC INIT_ARRAY/INIT_ARRAYSZ.

Referências

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks