反向工程原生库
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
更多信息请参见: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html
Android 应用可以使用原生库,通常用 C 或 C++ 编写,用于性能关键的任务。恶意软件作者也滥用这些库,因为 ELF 共享对象仍然比 DEX/OAT 字节码更难反编译。
本页侧重于实用工作流程以及近期(2023–2025)使反向 Android .so 文件更容易的工具改进。
新提取的 libfoo.so 的快速初步分析流程
- 提取库文件
# 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/
- 识别架构与保护
file libfoo.so # arm64 or arm32 / x86
readelf -h libfoo.so # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so # (peda/pwntools)
- 列出导出符号与 JNI 绑定
readelf -s libfoo.so | grep ' Java_' # dynamic-linked JNI
strings libfoo.so | grep -i "RegisterNatives" -n # static-registered JNI
- 在反编译器中加载 (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) 并运行自动分析。 新版 Ghidra 引入了 AArch64 反编译器,能识别 PAC/BTI stubs 和 MTE tags,大幅改善用 Android 14 NDK 构建的库的分析效果。
- 决定使用静态还是动态逆向: 被剥离或混淆的代码通常需要插桩(instrumentation)(Frida、ptrace/gdbserver、LLDB)。
动态插桩 (Frida ≥ 16)
Frida 16 系列带来了若干针对 Android 的改进,有助于在目标使用现代 Clang/LLD 优化时的调试与分析:
thumb-relocator现在可以 hook 由 LLD 的激进对齐(--icf=all)生成的微小 ARM/Thumb 函数。- 在 Android 上可以枚举并重新绑定 ELF import slots,当内联 hooks 被拒绝时,允许对每个模块进行
dlopen()/dlsym()层面的修补。 - 修复了针对新的 ART quick-entrypoint 的 Java hooking,该入口点在应用以
--enable-optimizations在 Android 14 上编译时使用。
示例:枚举所有通过 RegisterNatives 注册的函数并在运行时转储它们的地址:
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 在支持 PAC/BTI 的设备(Pixel 8/Android 14+)上可直接使用,只要你使用 frida-server 16.2 或更高版本 —— 早期版本无法定位 inline hooks 的 padding。
从内存中转储运行时解密的本地库 (Frida soSaver)
当受保护的 APK 将 native 代码加密或仅在运行时映射(packers, downloaded payloads, generated libs)时,附加 Frida 并直接从进程内存转储映射的 ELF。
soSaver 工作流程 (Python host + TS/JS Frida agent):
- Hooks
dlopen和android_dlopen_ext来检测加载时的库映射,并对已加载模块执行初步扫描。 - 定期扫描进程内存映射以查找 ELF headers,从而捕获通过 non-standard mappers 加载、从未经过 loader APIs 的模块。
- 以块为单位从内存读取每个模块,并通过 Frida messages 将字节流发送到 host;如果某区域无法读取,则在可用时回退到从 on-disk path 读取。
- 保存重建后的
.so文件并打印每模块的提取统计,提供用于 static RE 的工件。
运行 (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
该方法通过恢复内存中映射的实时镜像来绕过“仅在 RAM 解密”的防护,从而允许在 IDA/Ghidra 中进行离线分析,即使文件系统中的副本被混淆或不存在。
通过预加载 .so 在进程本地进行 JNI 遥测 (SoTap)
When full-featured instrumentation is overkill or blocked, you can still gain native-level visibility by preloading a small logger inside the target process. SoTap is a lightweight Android native (.so) library that logs the runtime behavior of other JNI (.so) libraries within the same app process (no root required).
关键特性:
- 早期初始化并观察加载它的进程内的 JNI/native 交互。
- 在多个可写路径中持久化日志,在存储受限时优雅地回退到 Logcat。
- 源码可定制:编辑 sotap.c 以扩展/调整记录内容,并按 ABI 重新构建。
Setup (repack the APK):
- 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)
- 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
- 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
Notes and troubleshooting:
- ABI 对齐是强制性的。若不匹配会抛出 UnsatisfiedLinkError 并且 logger 无法加载。
- 现代 Android 常有存储限制;如果文件写入失败,SoTap 仍会通过 Logcat 输出。
- 行为/详细程度预期可自定义;编辑 sotap.c 后从源码重建。
这种方法对 malware 分析和 JNI 调试很有用,特别是在需要从进程启动就观察 native 调用流而无法使用 root/系统范围 hook 的情况下。
See also: in‑memory native code execution via JNI
一种常见的攻击模式是在运行时下载原始 shellcode blob 并通过 JNI 桥直接从内存执行(不在磁盘上写入 ELF)。详细信息和可直接使用的 JNI 代码片段见:
In Memory Jni Shellcode Execution
Recent vulnerabilities worth hunting for in APKs
| Year | CVE | Affected library | Notes |
|---|---|---|---|
| 2023 | CVE-2023-4863 | libwebp ≤ 1.3.1 | 堆缓冲区溢出,可从解码 WebP 图像的 native 代码触发。若发现 APK 中包含 libwebp.so,请检查其版本并尝试利用或修补。 |
| 2024 | Multiple | OpenSSL 3.x series | 存在多种内存安全和 padding-oracle 问题。许多 Flutter & ReactNative 包会捆绑自己的 libcrypto.so。 |
当你在 APK 中发现 third-party .so 文件时,务必将其哈希与上游公告交叉核对。移动端很少做 SCA (Software Composition Analysis),因此过时且存在漏洞的构建很常见。
Anti-Reversing & Hardening trends (Android 13-15)
- Pointer Authentication (PAC) & Branch Target Identification (BTI): Android 14 在支持 ARMv8.3+ 的芯片上对系统库启用 PAC/BTI。反编译器现在会显示与 PAC 相关的伪指令;对于动态分析,Frida 在去除 PAC 后注入 trampolines,但你的自定义 trampolines 在必要时应调用
pacda/autibsp。 - MTE & Scudo hardened allocator: memory-tagging 为可选,但许多关注 Play-Integrity 的应用在构建时使用
-fsanitize=memtag;使用setprop arm64.memtag.dump 1加上adb shell am start ...来捕获 tag 错误。 - LLVM Obfuscator (opaque predicates, control-flow flattening): 商业 packer(例如 Bangcle、SecNeo)越来越多地保护 native 代码,而不仅仅是 Java;在
.rodata中会看到伪造的控制流和加密字符串块。
Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
高度保护的应用通常将 root/emulator/debug 检查放在通过 .init_array 非常早期运行的 native 构造函数中,这发生在 JNI_OnLoad 之前,并且远在任何 Java 代码执行之前。你可以通过以下方法将那些隐式初始化器显式化并重新获得控制权:
- 从 DYNAMIC 表中移除
INIT_ARRAY/INIT_ARRAYSZ,使加载器不再自动执行.init_array条目。 - 从 RELATIVE 重定位解析构造函数地址并将其导出为常规函数符号(例如
INIT0)。 - 将
JNI_OnLoad重命名为JNI_OnLoad0,以防 ART 隐式调用它。
Why this works on Android/arm64
- 在 AArch64 上,
.init_array条目通常由R_AARCH64_RELATIVE重定位在加载时填充,其 addend 即为.text内目标函数地址。 .init_array的字节在静态查看时可能看起来为空;动态链接器会在重定位处理过程中写入解析后的地址。
Identify the constructor target
- 使用 Android NDK toolchain 来对 AArch64 做准确的 ELF 解析:
# 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
- 找到落在
.init_array虚拟地址范围内的重定位;该R_AARCH64_RELATIVE的addend即为构造函数(例如0xA34,0x954)。 - 在该地址附近反汇编以进行合理性检查:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
Patch plan
- Remove
INIT_ARRAYandINIT_ARRAYSZDYNAMIC tags. Do not delete sections. - Add a GLOBAL DEFAULT FUNC symbol
INIT0at the constructor address so it can be called manually. - Rename
JNI_OnLoad→JNI_OnLoad0to stop ART from invoking it implicitly.
Validation after patch
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
使用 LIEF 打补丁(Python)
脚本:移除 INIT_ARRAY/INIT_ARRAYSZ,导出 INIT0,重命名 JNI_OnLoad→JNI_OnLoad0
```python import liefb = 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>
注意事项与失败尝试(为可移植性)
- 将 `.init_array` 字节清零或将节长度设为 0 无效:动态链接器会通过重定位重新填充它。
- 将 `INIT_ARRAY`/`INIT_ARRAYSZ` 设为 0 可能因标签不一致而破坏加载器。干净地移除那些 DYNAMIC 条目才是可靠的手段。
- 完全删除 `.init_array` 节通常会导致加载器崩溃。
- 打补丁后,函数/布局地址可能会移动;如果需要重新应用补丁,务必在已修补的文件上根据 `.rela.dyn` 的 addends 重新计算构造函数地址。
引导一个最小化的 ART/JNI 环境以调用 INIT0 和 JNI_OnLoad0
- 使用 JNIInvocation 在独立二进制中启动一个微型 ART VM 上下文。然后在任何 Java 代码之前手动调用 `INIT0()` 和 `JNI_OnLoad0(vm)`。
- 将目标 APK/classes 加入 classpath,这样任何 `RegisterNatives` 都能找到对应的 Java 类。
<details>
<summary>用于调用 INIT0 → JNI_OnLoad0 → Java 方法的最小化 harness(CMake 和 C)</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
常见陷阱:
- Constructor 地址在打补丁后会因为重排而改变;务必在最终二进制上从
.rela.dyn重新计算。 - 确保
-Djava.class.path覆盖RegisterNatives调用使用的每个类。 - 行为可能因 NDK/loader 版本而异;一致可靠的步骤是移除
INIT_ARRAY/INIT_ARRAYSZDYNAMIC tags。
References
- 学习 ARM Assembly: Azeria Labs – ARM Assembly Basics
- JNI & NDK 文档: Oracle JNI Spec · Android JNI Tips · NDK Guides
- 调试本机库: Debug Android Native Libraries Using JEB Decompiler
- Frida 16.x 变更日志 (Android hooking, tiny-function relocation) – frida.re/news
- NVD 针对
libwebp溢出 CVE-2023-4863 的通告 – nvd.nist.gov - SoTap:轻量级应用内 JNI (.so) 行为记录器 – github.com/RezaArbabBot/SoTap
- SoTap 发布 – github.com/RezaArbabBot/SoTap/releases
- 如何使用 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
- soSaver — 基于 Frida 的用于 Android
.so库的实时内存转储器 – github.com/TheQmaks/sosaver - soSaver Frida agent (TypeScript/JS) – github.com/TheQmaks/soSaver-frida
Tip
学习和实践 AWS 黑客技术:
HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。


