Reversão de Bibliotecas Nativas
Reading time: 12 minutes
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
- Confira os planos de assinatura!
 - Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
 - Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
 
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 os objetos compartilhados ELF ainda são mais difíceis de decompilar do que o byte-code DEX/OAT.
Esta página foca em fluxos de trabalho práticos e em melhorias recentes de ferramentas (2023–2025) que tornam a reversão de arquivos .so do Android mais fácil.
Fluxo rápido de triagem para um libfoo.so recém-extraído
- Extraia 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/
- Identifique 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)
- Liste 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
- Carregue em um decompilador (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) e execute 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 NDK do Android 14.
 - Decida entre reversão estática ou dinâmica: código sem símbolos e ofuscado frequentemente precisa de instrumentação (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-relocatoragora pode hook tiny ARM/Thumb functions geradas pelo alinhamento agressivo do LLD (--icf=all).- A enumeração e rebinding de ELF import slots funciona no Android, habilitando patching por módulo via 
dlopen()/dlsym()quando inline hooks são rejeitados. - Java hooking foi corrigido para o novo ART quick-entrypoint usado quando apps são compilados com 
--enable-optimizationsno Android 14. 
Exemplo: enumerando todas as funções registradas via RegisterNatives e imprimindo 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 funciona imediatamente em dispositivos com PAC/BTI (Pixel 8/Android 14+) desde que você use frida-server 16.2 ou posterior – versões anteriores não conseguiam localizar o padding para inline hooks.
Telemetria JNI local ao processo via .so pré-carregado (SoTap)
Quando a instrumentação completa é excessiva ou bloqueada, você ainda pode obter visibilidade em 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).
Principais características:
- Inicializa cedo e observa as interações JNI/nativas dentro do processo que a carrega.
 - Persiste logs usando múltiplos caminhos graváveis com fallback elegante para Logcat quando o armazenamento está restrito.
 - Personalizável no código-fonte: edite sotap.c para estender/ajustar o que é registrado e reconstrua por ABI.
 
Configuração (reempacote o APK):
- Coloque a build adequada para o ABI dentro do APK para que o loader possa resolver libsotap.so:
 
- lib/arm64-v8a/libsotap.so (for arm64)
 - lib/armeabi-v7a/libsotap.so (for arm32)
 
- Garanta que SoTap seja carregado antes de outras libs JNI. Injete uma chamada cedo (por exemplo, no inicializador estático da subclasse Application ou onCreate) para que o logger seja inicializado primeiro. Exemplo de snippet Smali:
 
const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
- Reconstrua/assine/instale, execute o app e depois colete os logs.
 
Caminhos de log (verificados nesta ordem):
/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 solução de problemas:
- O alinhamento do ABI é obrigatório. Um desajuste levantará UnsatisfiedLinkError e o logger não será carregado.
 - Restrições de armazenamento são comuns no Android moderno; se gravações de arquivo falharem, SoTap ainda emitirá via Logcat.
 - O comportamento/verbosidade é destinado a ser customizado; reconstrua a partir do código-fonte após editar sotap.c.
 
Esta abordagem é útil para malware triage e JNI debugging onde observar fluxos de chamadas nativas desde o início do processo é crítico, mas hooks de root/system-wide não estão disponíveis.
See also: in‑memory native code execution via JNI
A common attack pattern is to download a raw shellcode blob at runtime and execute it directly from memory through a JNI bridge (no on‑disk ELF). Details and ready‑to‑use JNI snippet here:
In Memory Jni Shellcode Execution
Vulnerabilidades recentes que vale a pena procurar em APKs
| Year | CVE | Affected library | Notes | 
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | Heap buffer overflow reachable from native code that decodes WebP images. Several Android apps bundle vulnerable versions. When you see a libwebp.so inside an APK, check its version and attempt exploitation or patching. | 
| 2024 | Multiple | OpenSSL 3.x series | Several memory-safety and padding-oracle issues. Many Flutter & ReactNative bundles ship their own libcrypto.so. | 
Quando você encontrar .so de third-party dentro de um APK, sempre verifique seu hash contra upstream advisories. SCA (Software Composition Analysis) é incomum em mobile, então builds vulneráveis desatualizados são comuns.
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 habilita PAC/BTI em system libraries em silício compatível ARMv8.3+. Decompilers agora exibem pseudo-instruções relacionadas a PAC; para análise dinâmica Frida injeta trampolines after stripping PAC, mas seus trampolines customizados devem chamar 
pacda/autibspquando necessário. - MTE & Scudo hardened allocator: memory-tagging é opt-in, mas muitos apps conscientes do Play-Integrity compilam com 
-fsanitize=memtag; usesetprop arm64.memtag.dump 1maisadb shell am start ...para capturar tag faults. - LLVM Obfuscator (opaque predicates, control-flow flattening): packers comerciais (e.g., Bangcle, SecNeo) protegem cada vez mais o código nativo, não apenas Java; espere bogus control-flow e encrypted string blobs em 
.rodata. 
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (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 bem antes de qualquer código Java executar. Você pode tornar esses inicializadores implícitos em explícitos e recuperar o controle ao:
- Remover 
INIT_ARRAY/INIT_ARRAYSZda tabela DYNAMIC para que o loader não auto-execute as 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 (por exemplo, 
INIT0). - Renomear 
JNI_OnLoadparaJNI_OnLoad0para evitar que o ART o chame implicitamente. 
Por que isso funciona no Android/arm64
- No AArch64, as entradas 
.init_arrayfrequentemente são preenchidas em tempo de carregamento por relocationsR_AARCH64_RELATIVEcujo addend é o endereço da função alvo dentro de.text. - Os bytes de 
.init_arraypodem parecer vazios estaticamente; o dynamic linker escreve o endereço resolvido durante o processamento de relocations. 
Identificar o alvo do construtor
- Use o toolchain do Android NDK 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 intervalo de endereços virtuais de 
.init_array; oaddenddesseR_AARCH64_RELATIVEé o construtor (por exemplo,0xA34,0x954). - Disassemble em torno desse endereço para checagem de sanidade:
 
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Plano de patch
- Remova as tags DYNAMIC 
INIT_ARRAYeINIT_ARRAYSZ. Não delete seções. - Adicione um símbolo GLOBAL DEFAULT FUNC 
INIT0no endereço do construtor para que ele possa ser chamado manualmente. - Renomeie 
JNI_OnLoad→JNI_OnLoad0para 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 patch com LIEF (Python)
Script: remover INIT_ARRAY/INIT_ARRAYSZ, exportar INIT0, renomear JNI_OnLoad→JNI_OnLoad0
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')
Notas e abordagens que falharam (para portabilidade)
- Zerando os bytes de 
.init_arrayou definindo o comprimento da seção para 0 não ajuda: o dynamic linker repopulates it via relocations. - Definir 
INIT_ARRAY/INIT_ARRAYSZpara 0 pode quebrar o loader devido a tags inconsistentes. A remoção limpa dessas entradas DYNAMIC é a alavanca confiável. - Excluir a seção 
.init_arrayinteiramente tende a travar o loader. - Após o patch, os endereços de função/layout podem deslocar; sempre recalcule o construtor a partir dos addends de 
.rela.dynno arquivo patchado se precisar reexecutar o patch. 
Inicializando um ART/JNI mínimo para invocar INIT0 e JNI_OnLoad0
- Use JNIInvocation para spin up um pequeno contexto ART VM em um binário standalone. Então chame 
INIT0()eJNI_OnLoad0(vm)manualmente antes de qualquer código Java. - Inclua o APK/classes alvo no classpath para que qualquer 
RegisterNativesencontre suas classes Java. 
Harness mínimo (CMake e C) para chamar INIT0 → JNI_OnLoad0 → método Java
# 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
Erros Comuns:
- Os endereços dos construtores mudam após patching devido ao re-layout; sempre recalcule a partir de 
.rela.dynno binário final. - Garanta que 
-Djava.class.pathcubra todas as classes usadas pelas chamadas deRegisterNatives. - O comportamento pode variar com versões do NDK/loader; a etapa que se mostrou consistentemente confiável foi remover as tags DYNAMIC 
INIT_ARRAY/INIT_ARRAYSZ. 
Referências
- Aprendizado de Assembly ARM: Azeria Labs – ARM Assembly Basics
 - Documentação JNI & NDK: Oracle JNI Spec · Android JNI Tips · NDK Guides
 - Depuração de bibliotecas nativas: Debug Android Native Libraries Using JEB Decompiler
 - Frida 16.x change-log (Android hooking, tiny-function relocation) – frida.re/news
 - Aviso NVD para overflow do 
libwebpCVE-2023-4863 – nvd.nist.gov - SoTap: logger leve de comportamento in-app JNI (.so) – github.com/RezaArbabBot/SoTap
 - Releases do SoTap – github.com/RezaArbabBot/SoTap/releases
 - Como trabalhar com SoTap? – t.me/ForYouTillEnd/13
 - CoRPhone — JNI memory-only execution pattern and packaging
 - Patching Android ARM64 library initializers for easy Frida instrumentation and debugging
 - LIEF Project
 - JNIInvocation
 
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
- Confira os planos de assinatura!
 - Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
 - Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
 
HackTricks