反向工程原生库

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

更多信息请参见: https://maddiestone.github.io/AndroidAppRE/reversing_native_libs.html

Android 应用可以使用原生库,通常用 C 或 C++ 编写,用于性能关键的任务。恶意软件作者也滥用这些库,因为 ELF 共享对象仍然比 DEX/OAT 字节码更难反编译。 本页侧重于实用工作流程以及近期(2023–2025)使反向 Android .so 文件更容易的工具改进。


新提取的 libfoo.so 的快速初步分析流程

  1. 提取库文件
# 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. 识别架构与保护
file libfoo.so        # arm64 or arm32 / x86
readelf -h libfoo.so  # OS ABI, PIE, NX, RELRO, etc.
checksec --file libfoo.so  # (peda/pwntools)
  1. 列出导出符号与 JNI 绑定
readelf -s libfoo.so | grep ' Java_'     # dynamic-linked JNI
strings libfoo.so   | grep -i "RegisterNatives" -n   # static-registered JNI
  1. 在反编译器中加载 (Ghidra ≥ 11.0, IDA Pro, Binary Ninja, Hopper or Cutter/Rizin) 并运行自动分析。 新版 Ghidra 引入了 AArch64 反编译器,能识别 PAC/BTI stubs 和 MTE tags,大幅改善用 Android 14 NDK 构建的库的分析效果。
  2. 决定使用静态还是动态逆向: 被剥离或混淆的代码通常需要插桩(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 dlopenandroid_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):

  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

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

YearCVEAffected libraryNotes
2023CVE-2023-4863libwebp ≤ 1.3.1堆缓冲区溢出,可从解码 WebP 图像的 native 代码触发。若发现 APK 中包含 libwebp.so,请检查其版本并尝试利用或修补。
2024MultipleOpenSSL 3.x series存在多种内存安全和 padding-oracle 问题。许多 Flutter & ReactNative 包会捆绑自己的 libcrypto.so

当你在 APK 中发现 third-party .so 文件时,务必将其哈希与上游公告交叉核对。移动端很少做 SCA (Software Composition Analysis),因此过时且存在漏洞的构建很常见。


  • 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_RELATIVEaddend 即为构造函数(例如 0xA34, 0x954)。
  • 在该地址附近反汇编以进行合理性检查:
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40

Patch plan

  1. Remove INIT_ARRAY and INIT_ARRAYSZ DYNAMIC tags. Do not delete sections.
  2. Add a GLOBAL DEFAULT FUNC symbol INIT0 at the constructor address so it can be called manually.
  3. Rename JNI_OnLoadJNI_OnLoad0 to 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 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>

注意事项与失败尝试(为可移植性)
- 将 `.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_ARRAYSZ DYNAMIC tags。

References

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