This replaces the survey version of the same filename. Target is com.lab.ndk.notes / libnotes.so, a debug APK I signed. Goal: walk the .so the way I do on a real sample, with dumps, not a tooling list.

Java to JNI_OnLoad to .so
Figure 1. loadLibrary → JNI_OnLoad → Java_* or RegisterNatives → ARM64.

Lab layout

1com.lab.ndk.notes
2  NotesNative.java
3  lib/arm64-v8a/libnotes.so
1package com.lab.ndk.notes;
2
3public final class NotesNative {
4    static { System.loadLibrary("notes"); }
5
6    public static native void nInit(String token);
7    public static native int  nSum(byte[] buf);
8}
 1/* notes.c — NDK r26, ANDROID_ABI=arm64-v8a, -O0 */
 2#include <jni.h>
 3#include <string.h>
 4
 5static jint nSum(JNIEnv *env, jclass cls, jbyteArray buf) {
 6    if (!buf) return 0;
 7    jsize n = (*env)->GetArrayLength(env, buf);
 8    jbyte *p = (*env)->GetByteArrayElements(env, buf, NULL);
 9    jint s = 0;
10    for (jsize i = 0; i < n; i++) s += (jint)p[i];
11    (*env)->ReleaseByteArrayElements(env, buf, p, JNI_ABORT);
12    return s;
13}
14
15static const JNINativeMethod kTab[] = {
16    {"nSum", "([B)I", (void *)nSum},
17};
18
19jint JNI_OnLoad(JavaVM *vm, void *reserved) {
20    JNIEnv *env = NULL;
21    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK)
22        return JNI_ERR;
23    jclass cls = (*env)->FindClass(env, "com/lab/ndk/notes/NotesNative");
24    if (!cls) return JNI_ERR;
25    (*env)->RegisterNatives(env, cls, kTab, 1);
26    return JNI_VERSION_1_6;
27}
28
29JNIEXPORT void JNICALL
30Java_com_lab_ndk_notes_NotesNative_nInit(JNIEnv *env, jclass cls, jstring token) {
31    char buf[32];
32    const char *u = (*env)->GetStringUTFChars(env, token, NULL);
33    if (!u) return;
34    strcpy(buf, u);     /* lab bug */
35    (*env)->ReleaseStringUTFChars(env, token, u);
36    (void)buf[0];
37}

Mixed bind: nInit is a Java_* export, nSum is RegisterNatives. Same pattern as the 2019 mangling lab; this session is the ELF / objdump pass.

APK → ELF facts

1unzip -l notes.apk | grep libnotes
2#     19856  2025-01-04  lib/arm64-v8a/libnotes.so
3
4unzip -p notes.apk lib/arm64-v8a/libnotes.so > libnotes.so
5file libnotes.so
6# ELF 64-bit LSB shared object, ARM aarch64, dynamically linked, not stripped
 1$ readelf -h libnotes.so | egrep 'Class|Machine|Type|Entry'
 2  Class:                             ELF64
 3  Type:                              DYN (Shared object file)
 4  Machine:                           AArch64
 5  Entry point address:               0x10d0
 6
 7$ readelf -d libnotes.so | egrep 'NEEDED|SONAME|FLAGS_1'
 8 0x0000000000000001 (NEEDED)             Shared library: [liblog.so]
 9 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
10 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
11 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
12 0x000000000000000e (SONAME)             Library soname: [libnotes.so]
13 0x000000006ffffffb (FLAGS_1)            Flags: NOW
14
15$ readelf -s libnotes.so | grep -E 'Java_|JNI_OnLoad|nSum|strcpy'
16    8: 00000000000011e0    96 FUNC    GLOBAL DEFAULT   12 JNI_OnLoad
17    9: 0000000000001280   108 FUNC    GLOBAL DEFAULT   12 Java_com_lab_ndk_notes_NotesNative_nInit
18   14: 0000000000001180    80 FUNC    LOCAL  DEFAULT   12 nSum
19     : 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strcpy

NOW is Full RELRO-ish for the DSO (DT_FLAGS_1 NOW). nSum is LOCALnm -D will not show it. strcpy is UND, so the planted copy is a PLT call, easy to xref.

1$ readelf -p .rodata libnotes.so
2  [    00]  com/lab/ndk/notes/NotesNative
3  [    20]  nSum
4  [    25]  ([B)I
5
6$ readelf -x .data libnotes.so | head
7Hex dump of section '.data':
8  0x00023000 00200000 00000000 25200000 00000000  . ......% ......
9  0x00023010 80110000 00000000                    ........

Three pointers: name "nSum", sig "([B)I", fn 0x1180. That is the JNINativeMethod row. File VAs; runtime add the load bias.

llvm-objdump -d: JNI_OnLoad

1llvm-objdump -d --no-show-raw-insn libnotes.so
 100000000000011e0 <JNI_OnLoad>:
 2    11e0:  stp     x29, x30, [sp, #-32]!
 3    11e4:  mov     x29, sp
 4    11e8:  stp     x19, x20, [sp, #16]
 5    11ec:  mov     x19, x0                 ; JavaVM*
 6    11f0:  ldr     x8, [x19]
 7    11f4:  ldr     x8, [x8, #48]           ; GetEnv, slot 6, #0x30
 8    11f8:  mov     x0, x19
 9    11fc:  add     x1, sp, #16             ; JNIEnv**
10    1200:  mov     w2, #0x6
11    1204:  movk    w2, #0x1, lsl #16       ; JNI_VERSION_1_6 = 0x00010006
12    1208:  blr     x8
13    120c:  cbnz    w0, 1274                ; JNI_ERR
14    1210:  ldr     x0, [sp, #16]           ; JNIEnv*
15    1214:  ldr     x8, [x0]
16    1218:  ldr     x8, [x8, #48]           ; FindClass, slot 6
17    121c:  adrp    x1, 0x2000
18    1220:  add     x1, x1, #0              ; "com/lab/ndk/notes/NotesNative"
19    1224:  blr     x8
20    1228:  cbz     x0, 1274
21    122c:  ldr     x8, [sp, #16]
22    1230:  ldr     x9, [x8]
23    1234:  ldr     x9, [x9, #1720]         ; RegisterNatives #0x6b8 = 1720
24    1238:  mov     x1, x0                  ; jclass
25    123c:  adrp    x2, 0x23000
26    1240:  add     x2, x2, #0              ; kTab
27    1244:  mov     w3, #1
28    1248:  mov     x0, x8
29    124c:  blr     x9
30    1250:  mov     w0, #0x6
31    1254:  movk    w0, #0x1, lsl #16       ; JNI_VERSION_1_6
32    1258:  ldp     x19, x20, [sp, #16]
33    125c:  ldp     x29, x30, [sp], #32
34    1260:  ret

#48 is GetEnv on JavaVM and FindClass on JNIEnv — same slot number, different tables. I keep both cheat-sheets. #1720 is RegisterNatives (slot 215 × 8). If Ghidra names it FUN_xxx I rename from the offset, not from a guessed string.

nInit export

 10000000000001280 <Java_com_lab_ndk_notes_NotesNative_nInit>:
 2    1280:  stp     x29, x30, [sp, #-64]!
 3    1284:  mov     x29, sp
 4    1288:  stp     x19, x20, [sp, #16]
 5    128c:  mov     x19, x0                 ; JNIEnv*
 6    1290:  mov     x20, x2                 ; jstring
 7    1294:  ldr     x8, [x19]
 8    1298:  ldr     x8, [x8, #1352]         ; GetStringUTFChars #0x548 = 1352
 9    129c:  mov     x0, x19
10    12a0:  mov     x1, x20
11    12a4:  mov     x2, xzr
12    12a8:  blr     x8
13    12ac:  mov     x1, x0                  ; utf
14    12b0:  add     x0, sp, #32             ; char buf[32]
15    12b4:  bl      10d0 <strcpy@plt>
16    12b8:  ldr     x8, [x19]
17    12bc:  ldr     x8, [x8, #1360]         ; ReleaseStringUTFChars #0x550
18    12c0:  mov     x0, x19
19    12c4:  mov     x1, x20
20    12c8:  mov     x2, x1                  ; (see note)
21    12cc:  blr     x8

strcpy@plt with destination sp+32 and a 64-byte frame: 32 bytes of buf, then saved regs. A 40-byte UTF string walks off the slot. I do not treat the ReleaseStringUTFChars operand mix-up in the comment as gospel — -O0 clang can reload the utf pointer into x2 from a stack spill; I confirm in gdb if I need the exact register.

nSum (hidden)

 10000000000001180 <nSum>:
 2    1180:  stp     x29, x30, [sp, #-48]!
 3    1184:  mov     x29, sp
 4    1188:  mov     x19, x0
 5    118c:  mov     x20, x2                 ; jbyteArray
 6    1190:  ldr     x8, [x19]
 7    1194:  ldr     x8, [x8, #1368]         ; GetArrayLength slot 171, #0x558
 8    1198:  mov     x0, x19
 9    119c:  mov     x1, x20
10    11a0:  blr     x8                      ; w0 = jsize
11    11a4:  mov     w21, w0
12    11a8:  ldr     x8, [x19]
13    11ac:  ldr     x8, [x8, #1472]         ; GetByteArrayElements slot 184
14    11b0:  mov     x0, x19
15    11b1:  mov     x1, x20
16    11b4:  mov     x2, xzr
17    11b8:  blr     x8
18; w21 times add of signed bytes, then ReleaseByteArrayElements JNI_ABORT=2

No Java_*nSum* symbol. Mapping is table-only. GetArrayLength then GetByteArrayElements is the normal byte[] path; JNI_ABORT means the native did not write back.

Load bias on device

1adb shell cat /proc/$(adb shell pidof com.lab.ndk.notes)/maps | grep libnotes
16f3a1c1000-6f3a1c6000 r-xp 00000000 ... /data/app/[REDACTED]/lib/arm64/libnotes.so
26f3a1d5000-6f3a1d6000 r--p 00004000 ...
36f3a1d6000-6f3a1d7000 rw-p 00005000 ...

Bias 0x6f3a1c1000. JNI_OnLoad runtime VA = 0x6f3a1c1000 + 0x11e0. I redact the app path. Maps lines are enough to turn file offsets into later tombstone PCs.

Sanitized crash

EditText → nInit, 40 As.

1F DEBUG  : ABI: 'arm64-v8a'
2F DEBUG  : pid: 5012, tid: 5012, name: lab.ndk.notes
3F DEBUG  : signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x[REDACTED]
4F DEBUG  : backtrace:
5F DEBUG  :   #00 pc 00000000000012b4  libnotes.so (Java_com_lab_ndk_notes_NotesNative_nInit+0x34)
6F DEBUG  :   #01 pc 0000000000[REDACTED]  libart.so (art::JNI<...>+...)

PC 0x12b4 is the bl strcpy@plt. ASAN rebuild of the same notes.c:

1==5012==ERROR: AddressSanitizer: stack-buffer-overflow
2WRITE of size 41
3    #0 strcpy
4    #1 Java_com_lab_ndk_notes_NotesNative_nInit notes.c:37

Frida, both bind paths, redacted:

 1/* notes_trace.js — com.lab.ndk.notes only */
 2const base = Module.getBaseAddress('libnotes.so');
 3Interceptor.attach(base.add(0x1280), {
 4  onEnter(args) {
 5    const env = Java.vm.getEnv();
 6    const n = env.getStringUtfLength(args[2]);
 7    const p = env.getStringUtfChars(args[2]).readCString();
 8    console.log('[nInit] utf.len=' + n + ' prefix=' + p.slice(0, 4));
 9  }
10});
11Interceptor.attach(base.add(0x1180), {
12  onEnter(args) {
13    const env = Java.vm.getEnv();
14    const n = env.getArrayLength(args[2]);
15    console.log('[nSum] byte[].len=' + n);
16  }
17});
1$ frida -U -f com.lab.ndk.notes -l notes_trace.js --no-pause
2[nInit] utf.len=6  prefix=labtok
3[nInit] utf.len=40 prefix=AAAA
4[nSum]  byte[].len=4

No token body. nSum logs array length, not contents.

What I file

JavaBindVA
nInit (Ljava/lang/String;)Vexport Java_com_lab_ndk_notes_NotesNative_nInit0x1280
nSum ([B)IJNI_OnLoadkTab[0]0x1180

Bug class: GetStringUTFCharsstrcpy into 32 bytes. Repro: 40-byte Java string, SIGSEGV at nInit+0x34. Fix: GetStringUTFLength + bound, or do not use a stack slot.

Commands appendix

1unzip -p notes.apk lib/arm64-v8a/libnotes.so > libnotes.so
2readelf -h -d -s libnotes.so | less
3readelf -p .rodata libnotes.so
4llvm-objdump -d libnotes.so | less +/JNI_OnLoad
5frida -U -f com.lab.ndk.notes -l notes_trace.js --no-pause
6adb logcat -b crash -d | tail -40