Skip to content

Commit ea8c204

Browse files
authored
Merge pull request #1512 from HackTricks-wiki/update_Patching_Android_ARM64_library_initializers_for_ea_20251021_183104
Patching Android ARM64 library initializers for easy Frida i...
2 parents ff2dc4f + 92510d2 commit ea8c204

File tree

1 file changed

+157
-4
lines changed

1 file changed

+157
-4
lines changed

src/mobile-pentesting/android-app-pentesting/reversing-native-libraries.md

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,172 @@ When you spot *third-party* `.so` files inside an APK, always cross-check their
130130

131131
---
132132

133-
### Resources
133+
### Neutralizing early native initializers (.init_array) and JNI_OnLoad for early instrumentation (ARM64 ELF)
134+
135+
Highly protected apps often place root/emulator/debug checks in native constructors that run extremely early via `.init_array`, before `JNI_OnLoad` and long before any Java code executes. You can make those implicit initializers explicit and regain control by:
136+
- Removing `INIT_ARRAY`/`INIT_ARRAYSZ` from the DYNAMIC table so the loader does not auto-execute `.init_array` entries.
137+
- Resolving the constructor address from RELATIVE relocations and exporting it as a regular function symbol (e.g., `INIT0`).
138+
- Renaming `JNI_OnLoad` to `JNI_OnLoad0` to prevent ART from calling it implicitly.
139+
140+
Why this works on Android/arm64
141+
- On AArch64, `.init_array` entries are often populated at load time by `R_AARCH64_RELATIVE` relocations whose addend is the target function address inside `.text`.
142+
- The bytes of `.init_array` may look empty statically; the dynamic linker writes the resolved address during relocation processing.
143+
144+
Identify the constructor target
145+
- Use the Android NDK toolchain for accurate ELF parsing on AArch64:
146+
```bash
147+
# Adjust paths to your NDK; use the aarch64-linux-android-* variants
148+
readelf -W -a ./libnativestaticinit.so | grep -n "INIT_ARRAY" -C 4
149+
readelf -W --relocs ./libnativestaticinit.so
150+
```
151+
- Find the relocation that lands inside the `.init_array` virtual address range; the `addend` of that `R_AARCH64_RELATIVE` is the constructor (e.g., `0xA34`, `0x954`).
152+
- Disassemble around that address to sanity check:
153+
```bash
154+
objdump -D ./libnativestaticinit.so --start-address=0xA34 | head -n 40
155+
```
156+
157+
Patch plan
158+
1) Remove `INIT_ARRAY` and `INIT_ARRAYSZ` DYNAMIC tags. Do not delete sections.
159+
2) Add a GLOBAL DEFAULT FUNC symbol `INIT0` at the constructor address so it can be called manually.
160+
3) Rename `JNI_OnLoad``JNI_OnLoad0` to stop ART from invoking it implicitly.
161+
162+
Validation after patch
163+
```bash
164+
readelf -W -d libnativestaticinit.so.patched | egrep -i 'init_array|fini_array|flags'
165+
readelf -W -s libnativestaticinit.so.patched | egrep 'INIT0|JNI_OnLoad0'
166+
```
167+
168+
Patching with LIEF (Python)
169+
170+
<details>
171+
<summary>Script: remove INIT_ARRAY/INIT_ARRAYSZ, export INIT0, rename JNI_OnLoad→JNI_OnLoad0</summary>
172+
173+
```python
174+
import lief
175+
176+
b = lief.parse("libnativestaticinit.so")
177+
178+
# Locate .init_array VA range
179+
init = b.get_section('.init_array')
180+
va, sz = init.virtual_address, init.size
181+
182+
# Compute constructor address from RELATIVE relocation landing in .init_array
183+
ctor = None
184+
for r in b.dynamic_relocations:
185+
if va <= r.address < va + sz:
186+
ctor = r.addend
187+
break
188+
if ctor is None:
189+
raise RuntimeError("No R_*_RELATIVE relocation found inside .init_array")
190+
191+
# Remove auto-run tags so loader skips .init_array
192+
for tag in (lief.ELF.DYNAMIC_TAGS.INIT_ARRAYSZ, lief.ELF.DYNAMIC_TAGS.INIT_ARRAY):
193+
try:
194+
b.remove(b[tag])
195+
except Exception:
196+
pass
197+
198+
# Add exported FUNC symbol INIT0 at constructor address
199+
sym = lief.ELF.Symbol()
200+
sym.name = 'INIT0'
201+
sym.value = ctor
202+
sym.size = 0
203+
sym.binding = lief.ELF.SYMBOL_BINDINGS.GLOBAL
204+
sym.type = lief.ELF.SYMBOL_TYPES.FUNC
205+
sym.visibility = lief.ELF.SYMBOL_VISIBILITY.DEFAULT
206+
207+
# Place symbol in .text index
208+
text = b.get_section('.text')
209+
for idx, sec in enumerate(b.sections):
210+
if sec == text:
211+
sym.shndx = idx
212+
break
213+
b.add_dynamic_symbol(sym)
214+
215+
# Rename JNI_OnLoad -> JNI_OnLoad0 to block implicit ART init
216+
j = b.get_symbol('JNI_OnLoad')
217+
if j:
218+
j.name = 'JNI_OnLoad0'
219+
220+
b.write('libnativestaticinit.so.patched')
221+
```
222+
</details>
223+
224+
Notes and failed approaches (for portability)
225+
- Zeroing `.init_array` bytes or setting the section length to 0 does not help: the dynamic linker repopulates it via relocations.
226+
- Setting `INIT_ARRAY`/`INIT_ARRAYSZ` to 0 can break the loader due to inconsistent tags. Clean removal of those DYNAMIC entries is the reliable lever.
227+
- Deleting the `.init_array` section entirely tends to crash the loader.
228+
- After patching, function/layout addresses might shift; always recompute the constructor from `.rela.dyn` addends on the patched file if you need to re-run the patch.
229+
230+
Bootstrapping a minimal ART/JNI to invoke INIT0 and JNI_OnLoad0
231+
- Use JNIInvocation to spin up a tiny ART VM context in a standalone binary. Then call `INIT0()` and `JNI_OnLoad0(vm)` manually before any Java code.
232+
- Include the target APK/classes on the classpath so any `RegisterNatives` finds its Java classes.
233+
234+
<details>
235+
<summary>Minimal harness (CMake and C) to call INIT0 → JNI_OnLoad0 → Java method</summary>
236+
237+
```cmake
238+
# CMakeLists.txt
239+
project(caller)
240+
cmake_minimum_required(VERSION 3.8)
241+
include_directories(AFTER ${CMAKE_SOURCE_DIR}/include)
242+
link_directories(${CMAKE_SOURCE_DIR}/lib)
243+
find_library(log-lib log REQUIRED)
244+
add_executable(caller "caller.c")
245+
add_library(jenv SHARED "jnihelper.c")
246+
target_link_libraries(caller jenv nativestaticinit)
247+
```
248+
249+
```c
250+
// caller.c
251+
#include <jni.h>
252+
#include "jenv.h"
253+
JavaCTX ctx;
254+
void INIT0();
255+
void JNI_OnLoad0(JavaVM* vm);
256+
int main(){
257+
char *jvmopt = "-Djava.class.path=/data/local/tmp/base.apk"; // include app classes
258+
if (initialize_java_environment(&ctx,&jvmopt,1)!=0) return -1;
259+
INIT0(); // manual constructor
260+
JNI_OnLoad0(ctx.vm); // manual JNI init
261+
jclass c = (*ctx.env)->FindClass(ctx.env, "eu/nviso/nativestaticinit/MainActivity");
262+
jmethodID m = (*ctx.env)->GetStaticMethodID(ctx.env,c,"stringFromJNI","()Ljava/lang/String;");
263+
jstring s = (jstring)(*ctx.env)->CallStaticObjectMethod(ctx.env,c,m);
264+
const char* p = (*ctx.env)->GetStringUTFChars(ctx.env,s,NULL);
265+
printf("Native string: %s\n", p);
266+
cleanup_java_env(&ctx);
267+
}
268+
```
269+
270+
```bash
271+
# Build (adjust NDK/ABI)
272+
cmake -DANDROID_PLATFORM=31 \
273+
-DCMAKE_TOOLCHAIN_FILE=$HOME/Android/Sdk/ndk/26.1.10909125/build/cmake/android.toolchain.cmake \
274+
-DANDROID_ABI=arm64-v8a ..
275+
make
276+
```
277+
</details>
278+
279+
280+
**Common Pitfalls:**
281+
- Constructor addresses change after patching due to re-layout; always recompute from `.rela.dyn` on the final binary.
282+
- Ensure `-Djava.class.path` covers every class used by `RegisterNatives` calls.
283+
- Behavior may vary with NDK/loader versions; the consistently reliable step was removing `INIT_ARRAY`/`INIT_ARRAYSZ` DYNAMIC tags.
134284

135-
- **Learning ARM Assembly:** [Azeria Labs – ARM Assembly Basics](https://azeria-labs.com/writing-arm-assembly-part-1/)
136-
- **JNI & NDK Documentation:** [Oracle JNI Spec](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html) · [Android JNI Tips](https://developer.android.com/training/articles/perf-jni) · [NDK Guides](https://developer.android.com/ndk/guides/)
137-
- **Debugging Native Libraries:** [Debug Android Native Libraries Using JEB Decompiler](https://medium.com/@shubhamsonani/how-to-debug-android-native-libraries-using-jeb-decompiler-eec681a22cf3)
138285

139286
### References
140287

288+
- **Learning ARM Assembly:** [Azeria Labs – ARM Assembly Basics](https://azeria-labs.com/writing-arm-assembly-part-1/)
289+
- **JNI & NDK Documentation:** [Oracle JNI Spec](https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html) · [Android JNI Tips](https://developer.android.com/training/articles/perf-jni) · [NDK Guides](https://developer.android.com/ndk/guides/)
290+
- **Debugging Native Libraries:** [Debug Android Native Libraries Using JEB Decompiler](https://medium.com/@shubhamsonani/how-to-debug-android-native-libraries-using-jeb-decompiler-eec681a22cf3)
141291
- Frida 16.x change-log (Android hooking, tiny-function relocation) – [frida.re/news](https://frida.re/news/)
142292
- NVD advisory for `libwebp` overflow CVE-2023-4863 – [nvd.nist.gov](https://nvd.nist.gov/vuln/detail/CVE-2023-4863)
143293
- SoTap: Lightweight in-app JNI (.so) behavior logger – [github.com/RezaArbabBot/SoTap](https://github.com/RezaArbabBot/SoTap)
144294
- SoTap Releases – [github.com/RezaArbabBot/SoTap/releases](https://github.com/RezaArbabBot/SoTap/releases)
145295
- How to work with SoTap? – [t.me/ForYouTillEnd/13](https://t.me/ForYouTillEnd/13)
146296
- [CoRPhone — JNI memory-only execution pattern and packaging](https://github.com/0xdevil/corphone)
297+
- [Patching Android ARM64 library initializers for easy Frida instrumentation and debugging](https://blog.nviso.eu/2025/10/14/patching-android-arm64-library-initializers-for-easy-frida-instrumentation-and-debugging/)
298+
- [LIEF Project](https://github.com/lief-project/LIEF)
299+
- [JNIInvocation](https://github.com/Ch0pin/JNIInvocation)
147300

148301
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)