[GH-ISSUE #6403] [Feature] Link Android native methods to their JNI implementations (Java_* exports and RegisterNatives tables) #3836

Closed
opened 2026-10-31 00:44:30 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6403

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Native analysis or decompilation

Current limitation

For an Android app, REA can tell me that a Java/Kotlin method is native, but not which function in which lib/<abi>/*.so implements it. Examined at REA main e83e3ceadd (source reading; the same Android tools are in the 6.7.0 release):

  • inspect_android_method identifies native methods from access flags and returns the native_method marker with an empty body; it names no library or symbol (docs/android-analysis.md, "Answer an analyst question", paragraph starting "Native and abstract methods are identified from access flags").
  • project_android_application_graph builds path-based bridge hypotheses as a DEX-class × native-library Cartesian product, and its documentation says the basis "does not identify a specific JNI binding" (docs/android-analysis.md, "Interpret the result").
  • inspect_managed_native_boundaries / verify_managed_native_boundaries compare P/Invoke declarations only (docs/managed-code-analysis.md, "Managed/native boundary rules").
  • src/, bridge/ and docs/ contain no handling of RegisterNatives or JNI_OnLoad.

Matching Java_* export names by hand (for example with inspect_native_exports on main) misses every method registered dynamically through RegisterNatives, which is common in real libraries. Observed on the public Conscrypt 2.8.0 Android AAR (Apache-2.0): its arm64-v8a libconscrypt_jni.so exports zero Java_* symbols and exports JNI_OnLoad, so every one of its native methods must be registered dynamically. I decoded one static JNINativeMethod entry from the file to confirm the table layout (details under Verification details).

Use cases

  • "Which native function implements NativeCrypto.EVP_PKEY_new_RSA?" when the library exports only JNI_OnLoad.
  • Scoping a native audit to the functions actually reachable from Java, instead of every export in every ABI.
  • Feeding the resolved addresses to analyze_function, procedure_pseudo_code or Ghidra seeds without first reverse-engineering JNI_OnLoad by hand.

Desired outcome and success criteria

For a selected APK (optionally narrowed to one class), report every ACC_NATIVE method with its candidate implementations per ABI and per library:

  1. Static exports: exact match of the JNI short name and the overloaded long name (__<mangled args>) against .dynsym, applying the JNI escapes (_1 for _, _2 for ;, _3 for [, and _0xxxx for non-ASCII characters). Results are reported per ABI; within one ABI each method gets matched, unmatched, or ambiguous (more than one library in that ABI exports the name), and the library identity stays attached to each match.
  2. Static RegisterNatives tables: decode JNINativeMethod {name, signature, fnPtr} arrays after applying the file's relocations (R_AARCH64_RELATIVE, R_X86_64_RELATIVE, R_386_RELATIVE, and R_ARM_RELATIVE, where 32-bit REL relocations keep the addend in the slot itself; and RELR where present), searching all writable data sections (the Conscrypt table is in .data, not .data.rel.ro). Accept an entry only when the name resolves to a NUL-terminated Modified UTF-8 string, the signature to a valid JNI method descriptor, and fnPtr lands in an executable PT_LOAD. An unsupported pointer encoding (for example Android packed APS2 relocations) is reported as such, never as "no tables found".
  3. A table entry does not name its class (the jclass comes from FindClass at run time). Attribute an entry to a DEX method only when exactly one DEX native method has that (name, descriptor) pair; otherwise report it as ambiguous with every candidate class. Any association derived from nearby FindClass strings is labelled as inferred.
  4. Evidence carries the APK and per-library SHA-256, ABI, table address, entry index, link-time address and file offset of each slot, and explicit coverage unknowns: tables built at run time, obfuscated strings, JNI_OnLoad present but no table decoded, unsupported relocation encodings.
  5. The result makes no runtime claim: a binding does not establish that the library is loaded or the method is called.

Success check: on the Conscrypt sample, NativeCrypto.EVP_PKEY_new_RSA with descriptor ([B[B[B[B[B[B[B[B)J resolves to the arm64 .text address supplied by its relocated fnPtr slot (0xfa11c in my reading), with the table entry's slot addresses as evidence; a self-built NDK fixture that uses both Java_* exports and RegisterNatives (including an overloaded method and a non-ASCII method name) resolves every method, and two classes registering the same (name, descriptor) produce ambiguous.

Expected interaction with targets

  • Read existing files or captured data

Target and scope details (optional)

Static analysis of the APK and its bundled ELF libraries only; no device, process or emulator. Non-goals: runtime confirmation of registrations (that is the debugger-correlation design in #2379), emulating JNI_OnLoad to recover tables built at run time, and JNA/UniFFI bindings (#4447). Related roadmap item: #717 ("selected native-library handoff and JNI relationship evidence"). #3489 notes that searching for Java_* exports misses RegisterNatives-bound methods.

Verification available so far

I can suggest a sample or manual check

Verification details (optional)

Observed with llvm-readelf and a byte read of jni/arm64-v8a/libconscrypt_jni.so from org.conscrypt:conscrypt-android:2.8.0 on Maven Central:

dynamic symbols named Java_*: 0
JNI_OnLoad: exported (FUNC GLOBAL, .text)
relocations: 6673 R_AARCH64_RELATIVE, 1692 JUMP_SLOT, 674 ABS64, 127 GLOB_DAT
JNINativeMethod entry at vaddr 0x2ada68 (.data):
  file bytes of the three slots: 0x0 0x0 0x0
  slot 0 (name)      R_AARCH64_RELATIVE -> 0x749a8 (.rodata) "EVP_PKEY_new_RSA"
  slot 1 (signature) R_AARCH64_RELATIVE -> 0x7affa (.rodata) "([B[B[B[B[B[B[B[B)J"
  slot 2 (fnPtr)     R_AARCH64_RELATIVE -> 0xfa11c (.text)

This shows that a table scan over raw section bytes finds nothing on a typical PIE arm64 NDK library; relocations must be applied first. The AAR ships Java classes as classes.jar, so an end-to-end APK check needs a small app that bundles it, or a self-built NDK fixture.

Alternatives considered

  • inspect_native_exports plus manual name mangling: covers static Java_* exports only; it returns nothing useful for Conscrypt.
  • Opening the .so in Ghidra and reading JNI_OnLoad by hand: possible, but token-heavy, per-library, and leaves the DEX side unlinked.
  • Frida or a debugger hooking RegisterNatives: requires running the app on a device, which is a different authority and lifecycle boundary.

Possible direction

REA already has building blocks: the engine-free ELF export parser behind inspect_native_exports (src/native/exports/NativeExportParser.ts, on main only), the AArch64/x86-64 DT_RELA RELATIVE relocation overlay used for IL2CPP (src/il2cpp/ElfRelocations.ts), the Android method access-flag facts, and the Ghidra seed-file writer used by IL2CPP (docs/il2cpp-metadata.md). Emitting function seeds for resolved implementations could be a later option.

References

Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6403 ### Before submitting - [X] I searched the open and closed issues and pull requests for a related proposal. - [X] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [X] This request does not include credentials, private samples, or sensitive target data. ### Area Native analysis or decompilation ### Current limitation For an Android app, REA can tell me that a Java/Kotlin method is `native`, but not which function in which `lib/<abi>/*.so` implements it. Examined at REA `main` e83e3ceadd97770a50b8afa97f4077916e4d2b81 (source reading; the same Android tools are in the 6.7.0 release): - `inspect_android_method` identifies native methods from access flags and returns the `native_method` marker with an empty body; it names no library or symbol (`docs/android-analysis.md`, "Answer an analyst question", paragraph starting "Native and abstract methods are identified from access flags"). - `project_android_application_graph` builds path-based bridge hypotheses as a DEX-class × native-library Cartesian product, and its documentation says the basis "does not identify a specific JNI binding" (`docs/android-analysis.md`, "Interpret the result"). - `inspect_managed_native_boundaries` / `verify_managed_native_boundaries` compare P/Invoke declarations only (`docs/managed-code-analysis.md`, "Managed/native boundary rules"). - `src/`, `bridge/` and `docs/` contain no handling of `RegisterNatives` or `JNI_OnLoad`. Matching `Java_*` export names by hand (for example with `inspect_native_exports` on `main`) misses every method registered dynamically through `RegisterNatives`, which is common in real libraries. Observed on the public Conscrypt 2.8.0 Android AAR (Apache-2.0): its arm64-v8a `libconscrypt_jni.so` exports zero `Java_*` symbols and exports `JNI_OnLoad`, so every one of its native methods must be registered dynamically. I decoded one static `JNINativeMethod` entry from the file to confirm the table layout (details under Verification details). ### Use cases - "Which native function implements `NativeCrypto.EVP_PKEY_new_RSA`?" when the library exports only `JNI_OnLoad`. - Scoping a native audit to the functions actually reachable from Java, instead of every export in every ABI. - Feeding the resolved addresses to `analyze_function`, `procedure_pseudo_code` or Ghidra seeds without first reverse-engineering `JNI_OnLoad` by hand. ### Desired outcome and success criteria For a selected APK (optionally narrowed to one class), report every `ACC_NATIVE` method with its candidate implementations per ABI and per library: 1. Static exports: exact match of the JNI short name and the overloaded long name (`__<mangled args>`) against `.dynsym`, applying the JNI escapes (`_1` for `_`, `_2` for `;`, `_3` for `[`, and `_0xxxx` for non-ASCII characters). Results are reported per ABI; within one ABI each method gets `matched`, `unmatched`, or `ambiguous` (more than one library in that ABI exports the name), and the library identity stays attached to each match. 2. Static `RegisterNatives` tables: decode `JNINativeMethod {name, signature, fnPtr}` arrays after applying the file's relocations (`R_AARCH64_RELATIVE`, `R_X86_64_RELATIVE`, `R_386_RELATIVE`, and `R_ARM_RELATIVE`, where 32-bit REL relocations keep the addend in the slot itself; and RELR where present), searching all writable data sections (the Conscrypt table is in `.data`, not `.data.rel.ro`). Accept an entry only when the name resolves to a NUL-terminated Modified UTF-8 string, the signature to a valid JNI method descriptor, and `fnPtr` lands in an executable `PT_LOAD`. An unsupported pointer encoding (for example Android packed APS2 relocations) is reported as such, never as "no tables found". 3. A table entry does not name its class (the `jclass` comes from `FindClass` at run time). Attribute an entry to a DEX method only when exactly one DEX native method has that `(name, descriptor)` pair; otherwise report it as ambiguous with every candidate class. Any association derived from nearby `FindClass` strings is labelled as inferred. 4. Evidence carries the APK and per-library SHA-256, ABI, table address, entry index, link-time address and file offset of each slot, and explicit coverage unknowns: tables built at run time, obfuscated strings, `JNI_OnLoad` present but no table decoded, unsupported relocation encodings. 5. The result makes no runtime claim: a binding does not establish that the library is loaded or the method is called. Success check: on the Conscrypt sample, `NativeCrypto.EVP_PKEY_new_RSA` with descriptor `([B[B[B[B[B[B[B[B)J` resolves to the arm64 `.text` address supplied by its relocated `fnPtr` slot (0xfa11c in my reading), with the table entry's slot addresses as evidence; a self-built NDK fixture that uses both `Java_*` exports and `RegisterNatives` (including an overloaded method and a non-ASCII method name) resolves every method, and two classes registering the same `(name, descriptor)` produce `ambiguous`. ### Expected interaction with targets - Read existing files or captured data ### Target and scope details (optional) Static analysis of the APK and its bundled ELF libraries only; no device, process or emulator. Non-goals: runtime confirmation of registrations (that is the debugger-correlation design in #2379), emulating `JNI_OnLoad` to recover tables built at run time, and JNA/UniFFI bindings (#4447). Related roadmap item: #717 ("selected native-library handoff and JNI relationship evidence"). #3489 notes that searching for `Java_*` exports misses `RegisterNatives`-bound methods. ### Verification available so far I can suggest a sample or manual check ### Verification details (optional) Observed with `llvm-readelf` and a byte read of `jni/arm64-v8a/libconscrypt_jni.so` from `org.conscrypt:conscrypt-android:2.8.0` on Maven Central: ```text dynamic symbols named Java_*: 0 JNI_OnLoad: exported (FUNC GLOBAL, .text) relocations: 6673 R_AARCH64_RELATIVE, 1692 JUMP_SLOT, 674 ABS64, 127 GLOB_DAT JNINativeMethod entry at vaddr 0x2ada68 (.data): file bytes of the three slots: 0x0 0x0 0x0 slot 0 (name) R_AARCH64_RELATIVE -> 0x749a8 (.rodata) "EVP_PKEY_new_RSA" slot 1 (signature) R_AARCH64_RELATIVE -> 0x7affa (.rodata) "([B[B[B[B[B[B[B[B)J" slot 2 (fnPtr) R_AARCH64_RELATIVE -> 0xfa11c (.text) ``` This shows that a table scan over raw section bytes finds nothing on a typical PIE arm64 NDK library; relocations must be applied first. The AAR ships Java classes as `classes.jar`, so an end-to-end APK check needs a small app that bundles it, or a self-built NDK fixture. ### Alternatives considered - `inspect_native_exports` plus manual name mangling: covers static `Java_*` exports only; it returns nothing useful for Conscrypt. - Opening the `.so` in Ghidra and reading `JNI_OnLoad` by hand: possible, but token-heavy, per-library, and leaves the DEX side unlinked. - Frida or a debugger hooking `RegisterNatives`: requires running the app on a device, which is a different authority and lifecycle boundary. ### Possible direction REA already has building blocks: the engine-free ELF export parser behind `inspect_native_exports` (`src/native/exports/NativeExportParser.ts`, on `main` only), the AArch64/x86-64 `DT_RELA` RELATIVE relocation overlay used for IL2CPP (`src/il2cpp/ElfRelocations.ts`), the Android method access-flag facts, and the Ghidra seed-file writer used by IL2CPP (`docs/il2cpp-metadata.md`). Emitting `function` seeds for resolved implementations could be a later option. ### References - Design cautions: a scan over raw section bytes finds no tables in PIE libraries (see Verification details); keying registered entries only by `(name, signature)` loses the class; and merging exports from every library and ABI into one name-keyed map loses which library implements a method. - JNI specification, "Resolving Native Method Names": https://docs.oracle.com/en/java/javase/21/docs/specs/jni/design.html#resolving-native-method-names - Sample: https://repo1.maven.org/maven2/org/conscrypt/conscrypt-android/2.8.0/ - Related: #717, #2379, #3489, #4447.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3836
No description provided.