Entry Decision Gate
When the target appears, first determine its form; once matched, switch to the corresponding skill (automatic scheduling and routing):
目标
├── APK
│ ├── lib/*.so 存在?──是──→ 本技能(re-android-native)
│ └── 无 so(纯 Java)──→ re-apk
├── 单独 .so
│ ├── JNI 符号(Java_* / JNI_OnLoad / RegisterNatives 调用点)──→ 本技能
│ ├── 加密库 API(SSL_*/EVP_*/crypto_* 等)──→ re-android-crypto(加密语义)
│ └── 普通 ELF(无 JNI 特征)──→ re-binary-core(re-format-elf)
├── 加固 so(壳壳/熵高/导入表极小)──→ re-anti-analysis / re-mobile-pack(脱壳后回本技能)
└── 加密体系审计目标(Keystore/Cipher)──→ re-android-crypto
When to Use / When Not to Use
- Use: the
lib/*.sonative library in the APK — JNI interface restoration, registration method (static/dynamic), native logic analysis - Use: Java-layer logic is clear but the core algorithm/anti-debugging is in native (games, hardening, encryption logic)
- Use: only
System.loadLibrary+nativedeclarations are visible at the Java layer, and the native implementation needs to be located and restored - Do not use: if you only need the APK Java layer statically (use [[re-apk]]); if you only need runtime hooking without needing to understand the .so structure (use [[re-frida]])
- Do not use: if the so is a hardening/packer shell (first unpack via [[re-mobile-pack]] / [[re-anti-analysis]], see Pitfall 5)
- Note: dynamic instrumentation must be executed within a controlled device / emulator snapshot per the highest principle of [[re-analyze/platform-tips]]
Tool Preparation
Static analysis (readelf / Ghidra importing .so) does not require a sandbox; dynamic (frida / xhook) must be executed in a controlled environment per the highest principle of [[re-analyze/platform-tips]]. Verify all tools before use.
Decompilation workbench (so loading) — [[re-ghidra]] / [[re-ida]]
- [[re-ghidra]] (default):
File > Import Filedirectly import ELF .so (arm64/arm/x86_64 all supported), load jni.h types via Data Type Manager (see [[re-ghidra]] Tool Preparation) - [[re-ida]]: after loading the so, decompile for ARM64;
Option > Load new typeload jni.h - Verification: after importing an arm64 .so, it can decompile the
JNI_OnLoadorJava_*exported functions
jni.h — understanding the JNIEnv function table structure (bundled with the Android NDK)
- No standalone distribution package; provided with the Android NDK. Official way: install the NDK via Android Studio SDK Manager, or via command line:
sdkmanager "ndk;27.2.12479018" # 先确认 sdkmanager 在 PATH(Android SDK cmdline-tools) # 或 GitHub android-ndk 官方仓库下载对应版本 zip - Key files:
$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/jni.h(on macOS the path is darwin-x86_64) - Verification: can open jni.h and see the definitions of
JNINativeInterface(function table) andJNIEnv(function table pointer)
readelf (binutils) — export table/architecture identification (in conjunction with [[re-format-elf]])
- Linux:
apt install binutils/dnf install binutils/pacman -S binutils - macOS:
brew install binutils(greadelf) - Verification:
readelf --version
frida ([[re-frida]]) — JNI runtime observation
- Host
pip install frida-tools; for device frida-server push installation see [[re-frida]] Tool Preparation - Verification:
frida-ps -Ucan list device processes
adb — extract so from device / install APK
- Linux:
apt install adb(Debian/Ubuntu)/dnf install android-tools/pacman -S android-tools - macOS:
brew install android-platform-tools; Windows: official platform-tools zip - Verification:
adb version
Operation Steps
Execute in order; for each step record the artifact (so path, readelf output, Ghidra project, frida script) with evidence path + sha256 (see [[re-triage]]), for reference in the report.
-
so extraction and architecture identification:
unzip app.apk -d app/ # APK 是 zip find app -name "*.so" # lib/ 下所有原生库 file app/lib/arm64-v8a/libtarget.so # 确认架构(ELF 64-bit ARM aarch64) readelf -h app/lib/arm64-v8a/libtarget.so # Machine: AArch64 / x86-64 readelf -d app/lib/arm64-v8a/libtarget.so # 依赖库与动态段- ABI directories:
arm64-v8a(main on real devices),armeabi-v7a(32-bit),x86_64/x86(emulator) — for multi-ABI differences see Pitfall 4 - Installed apps:
adb pull /data/app/.../lib/arm64-v8a /tmp/lib/(requires root or debuggable) - Record: target ABI, export table size, dependencies (
readelf -dNEEDED), whether stripped (readelf -snumber of symbols)
- ABI directories:
-
JNI interface recovery (JNIEnv function table):
readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i java # 静态注册的 Java_* 导出 readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i JNI # JNI_OnLoad / JNI_OnUnload- JNIEnv is a function table pointer:
JNIEnv* envactually points to theJNINativeInterfacefunction table (defined in jni.h), and function calls are(*env)->GetStringUTFChars(env, ...)-style table slot accesses—seeing "get a function pointer from a struct offset and then call it" in decompilation means it is a JNI API, see pitfall 1 - After loading the jni.h types in Ghidra/IDA, recover by name:
GetStringUTFChars(Java jstring → Modified UTF-8 byte sequence)、NewStringUTF(Modified UTF-8 → Java String)、CallVoidMethod/CallBooleanMethod(calls back into Java),GetJavaVM(gets the JavaVM in-process) - is Modified UTF-8, not standard UTF-8: the two differ in the encoding of the null character (
0xC0 0x80) and supplementary characters (surrogate pairs)—parsing as standard UTF-8 yields incorrect strings, andstrlen-class operations done by byte count will also mismatch (the character count must be obtained using JNI's own length interface) - Recovery goal: native function signature (
(JNIEnv*, jclass/jobject, 业务参数...))—the first parameter is env, the second is jclass (static) or jobject (instance), and business parameters start from the third
- JNIEnv is a function table pointer:
-
Registration method (static JNI_OnLoad / dynamic RegisterNatives):
- Static registration: function name
Java_包名_类名_方法名(underscore escaping), appears directly in the export table (visible in step 2) - Dynamic registration:
JNI_OnLoadcallsRegisterNatives(env, clazz, methods, count),methodsis aJNINativeMethod{name, signature, fnPtr}array—all three fields are pointers, and the width varies with the ABI(8 bytes each on 64-bit;4 bytes each on 32-bit ABIs such as armeabi-v7a / x86, array stride 12 bytes)—the function addresses are not in the export table(see pitfall 2); in decompilation locateJNI_OnLoadand then recover item by item along the third argument array of RegisterNatives - Mechanism key points (knowledge layer):
JNIEnv*points to theJNINativeInterfacefunction table (see pitfall 1),RegisterNativesis located in the standardJNIEnvfunction table index 215(UnregisterNatives216)—this is the fixed layout of the JNI ABI; actual byte offset = index × target pointer width (215×8=0x6B8 on 64-bit). Android Dalvik/ART, the NDK, and standardjni.hdo not change this index - frida observes runtime registration (spawn the target App): for the script template and runtime verification strategy (
JNIEnv*validity / hook table detection / ABI argument-passing branches) see [[probes]]—what is volatile is the ABI argument registers and libart internal structures, so rely on actual measurements in the target environment - Recovery output:
Java 方法名 → 签名 → native 函数地址mapping table
- Static registration: function name
-
Logic analysis ([[re-ghidra]] linkage):
- Import the so into Ghidra → load the jni.h types (step 2) → F5 decompile each native function
- When there is no symbolic aid, infer from API call sites: the parameters of the JNI API are the entry points of business data (e.g., the return value of
GetStringUTFCharsis the input string) - Dig deep into algorithm/verification/decryption logic using the [[re-binary-core]] methodology; if obfuscated (OLLVM mutation/string encryption) → switch to [[re-deobfuscate]] (pitfall 3)
- Statically registered
Java_*functions often have cross-calls (native internal function tables/callbacks), so trace the source of function pointers ([[re-binary-core]] R6 idea)
-
Interacting with the Java layer (locating native call sites):
- Java side ([[re-apk]] / jadx):
System.loadLibrary("target")andnativethe class where the method declaration resides——Java_包名_类名_*the naming comes from here - In jadx, for each
nativemethod find the call sites (who triggers it on what business path), align with the comparison table from step 3, and form theJava 调用点 ↔ native 函数mapping - Dynamic verification: in frida
Java.performdirectly call the native method (Java.use("com.x.Cls").method(...)) to observe parameters and return value; or use the xhook / PLT hook approach ([[re-frida]]'sInterceptor.attach(Process.getModuleByName(...).findExportByName(...))) to observe the native internal call chain to external libraries (libc / system libraries) - Closed loop: Java trigger point → parameter source → native processing logic → output back to the Java layer
- Java side ([[re-apk]] / jadx):
Cross-domain combination
- [[re-mobile]]: This skill is a specialized sub-skill of step 4 of its workflow (native libraries)——once the gateway identifies a target containing
.soit is dispatched by fixed rule - [[re-frida]]: Dynamically observe RegisterNatives / hook native functions / Java↔native interaction verification (spawn to catch JNI_OnLoad)
- [[re-format-elf]]: Export table / dynamic segment / symbol resolution (readelf three tables), a prerequisite for locating dynamically registered functions
- [[re-ghidra]]: so decompilation workbench + jni.h type loading
- [[re-apk]]: Java side static (jadx to find native declarations and call sites); after hardening identification, switch to the unpacking domain
- [[re-deobfuscate]]: Restoration of OLLVM / string-encrypted native code
- [[re-mobile-pack]] / [[re-anti-analysis]]: For hardened so packers, unpack first then analyze (pitfall 5)
- [[re-android-crypto]]: Cryptographic system auditing (Keystore/Cipher/third-party crypto library semantics) is already handled independently——this skill focuses on JNI/native logic; the cryptographic semantics of crypto library APIs inside .so are transferred to [[re-android-crypto]]
- [[re-analyze]]: Invoked by the triage "mobile App analysis" path (re-mobile → native libraries → this skill)
- [[re-analyze/platform-tips]]: The highest principle for dynamic instrumentation in a controlled environment
Common pitfalls and traps
- JNIEnv is a function table pointer (not a direct call): Symptom——In decompilation, JNI function call sites look like "take a function pointer from a struct offset and then call it", parameters don't match, or it is analyzed as an ordinary function
GetStringUTFCharsand mistakenly used directly as a string function; Cause——JNIEnvpoints to theJNINativeInterfacefunction table, all JNI APIs are function pointers in table slots, the C form(*env)->fn(env, ...); Countermeasure——Load the jni.h types in Ghidra/IDA (import via Data Type Manager), declareJNIEnvasJNINativeInterface**, decompilation automatically restores it to theenv->GetStringUTFChars(env, str)form; when there is no type library, manually build the struct byJNINativeInterfaceslot index - Dynamically registered function addresses are not in the export table: Symptom——
readelf --dyn-syms/ strings cannot findJava_*or business function names, IDA is full of addresses with no names; Cause——When dynamically registered, functions are static/local symbols, and only at runtime doesRegisterNativesbind addresses to Java methods; Countermeasure——JNI_OnLoadis an optional library load hook, not necessarily exported: if present, decompile and inspectRegisterNativesthe third-parameter array item by item to restore (name/signature/fnPtr each is a pointer: 64-bit 8 bytes, 32-bit 4 bytes); when it does not exist (or the export table does not contain it), obtain it fromRegisterNativesother call sites, JNI function table xrefs, string location, or after frida spawn hook RegisterNatives (step 3 script) to directly get the runtime registration table - Obfuscated native (OLLVM): Symptom - decompilation is all control-flow flattening (switch dispatcher), strings are all encrypted, functions are huge and hard to read; Cause - game/hardening vendors use OLLVM (bcf, sub) or commercial obfuscation (VMP-like) to protect native code; Countermeasure - first confirm the obfuscation type (flattening vs instruction virtualization), for flattening restore according to [[re-deobfuscate]] (state variable + case blocks), for string encryption locate the decryption function then batch decrypt with a script; if still not possible, use frida dynamically to get the runtime plaintext (hook the decryption function and read memory)
- Multi-ABI architecture differences: Symptom - offsets/instructions analyzed for arm64 are all wrong when applied to armeabi-v7a, or behavior on the x86_64 emulator differs from a real device; Cause - the APK has one so per ABI, and compilation optimization/instruction set/calling convention differ (arm64 uses x0-x7 to pass arguments, arm32 has thumb instructions, x86_64 uses rdi/rsi...), and some so files even return different implementations depending on ABI (e.g., arm64 real device vs x86_64 emulator branches); Countermeasure -
file/readelf -hfirst confirm the target ABI, analyze based on the real-device ABI (arm64-v8a), and treat x86_64 results as reference only; JNI type widths are consistent across ABIs (jlong=64-bit, jint=32-bit) but at the C levellongwidths differ, so pay attention to the type annotations in the decompilation - Hardened so (packer): Symptom - static analysis of the so shows only a small stub / shell code,
JNI_OnLoaddecompilation is an unpacking process; Cause - the so is hardened (vendor packer / commercial packer), and the real logic is only decrypted into memory at runtime; Countermeasure - first identify the hardening ([[re-apk]] hardening identification + entropy), for static unpacking follow [[re-mobile-pack]], or at runtime use [[re-memdump]] to extract the decrypted so from memory and then analyze it; archive the sha256 of the unpacking product and return to step 1 to rerun
Experience library
For the full collection of practical experience, see [[experience]].