|
6 | 6 |
|
7 | 7 | Supply-chain tampering of `/system/lib[64]/libandroid_runtime.so` can hijack `android.util.Log.println_native` so that **every app forked from Zygote executes attacker code**. The Keenadu backdoor adds a single call inside `println_native` that drives a native dropper. Because all app processes run this code, Android sandbox boundaries and per-app permissions are effectively bypassed. |
8 | 8 |
|
| 9 | +The same **post-root execution model** also appears in multi-stage Android rootkits delivered from apparently benign Play-distributed apps. In McAfee's **Operation NoVoice** analysis, the malware first landed in user space, obtained root with device-tailored exploits, and then **replaced `libandroid_runtime.so` and `libmedia_jni.so` on the system partition** so that every app spawned by `zygote` inherited the attacker hooks after reboot. |
| 10 | + |
| 11 | +## Pre-root staging: SDK init hijack + PNG tail polyglot |
| 12 | + |
| 13 | +Before the `libandroid_runtime.so` replacement, NoVoice used an app-level bootstrap that is worth recognizing during APK triage: |
| 14 | + |
| 15 | +- **Auto-exec on first launch**: bootstrap code ran from a tampered Facebook SDK init path, so no extra user interaction or suspicious permission prompt was required. |
| 16 | +- **Payload smuggling in assets**: the app shipped a valid **PNG** with an encrypted blob appended **after the PNG `IEND` marker**. Android image decoders ignore trailing bytes, but the loader carved the tail into `enc.apk`, decrypted it into `h.apk`, loaded it, and deleted the staging files. |
| 17 | +- **Triage clue**: if bytes immediately after `IEND` begin with a magic such as `CAFEBABE`, assume the image is acting as a **polyglot carrier** for a Java class / JAR / APK payload rather than as a pure media asset. |
| 18 | + |
| 19 | +Quick checks: |
| 20 | + |
| 21 | +```bash |
| 22 | +pngcheck -v suspicious.png |
| 23 | +tail -c +1 suspicious.png | xxd | tail |
| 24 | +binwalk -e suspicious.png |
| 25 | +``` |
| 26 | + |
| 27 | +Hunting notes: |
| 28 | + |
| 29 | +- Search APK `assets/` for PNGs with **unexpected trailing bytes** or high entropy after `IEND`. |
| 30 | +- Trace early init paths such as `Application.onCreate`, third-party SDK bootstrap code, or native `JNI_OnLoad` handlers that open asset PNGs and then write `*.apk` / `*.jar` files into the app sandbox. |
| 31 | + |
9 | 32 | ## Dropper path: native patch → RC4 → DexClassLoader |
10 | 33 | - Hooked entry: extra call inside `println_native` to `__log_check_tag_count` (injected static lib `libVndxUtils.a`). |
11 | 34 | - Payload storage: RC4-decrypt blob embedded in the `.so`, drop to `/data/dalvik-cache/arm[64]/system@framework@vndx_10x.jar@classes.jar`. |
@@ -40,13 +63,34 @@ struct KeenaduPayload { |
40 | 63 | - Integrity: MD5 file check + DSA signature (only operator with private key can issue modules). |
41 | 64 | - Decryption: AES-128-CFB, key `MD5("37d9a33df833c0d6f11f1b8079aaa2dc" + salt)`, IV `"0102030405060708"`. |
42 | 65 |
|
| 66 | +## Post-root persistence: wrapper libraries + framework patching + self-heal |
| 67 | + |
| 68 | +Once root is available, replacing `libandroid_runtime.so` is only one layer of persistence. NoVoice shows a more resilient pattern: |
| 69 | + |
| 70 | +- **Wrapper replacement instead of inline patching**: the installer backed up the original system library and replaced it with an **architecture-matched hook wrapper**. The same campaign also replaced `libmedia_jni.so`, giving multiple code-execution choke points inside the framework. |
| 71 | +- **Second-stage persistence in framework bytecode**: after the library swap, a dedicated patcher modified **pre-compiled Android framework bytecode on disk**. This means restoring the original `.so` may still leave injected redirections active. |
| 72 | +- **Self-healing watchdog**: a daemon checked the installation roughly every 60 seconds, restored missing components, and could **force a reboot** if reinsertion kept failing. The malware also replaced the system crash handler / recovery flow so rebooting re-launched the rootkit. |
| 73 | +- **Per-app payload assembly at runtime**: after reboot, the replaced `libandroid_runtime.so` caused each spawned app to load attacker code. NoVoice stored secondary payloads as fragments inside the malicious library, assembled them in memory, and deleted the disk copies immediately after load. |
| 74 | + |
| 75 | +Practical implications: |
| 76 | + |
| 77 | +- **Factory reset is insufficient** when the malware has modified the **system partition** or framework artifacts. Reflash the firmware instead. |
| 78 | +- Diff both **`/system/lib*/libandroid_runtime.so`** and **framework oat/odex/vdex artifacts**; checking only the shared library can miss the bytecode persistence layer. |
| 79 | +- If a device keeps restoring the malicious library after manual cleanup, look for a **watchdog daemon**, modified recovery scripts, or a replaced crash-handler path that is re-seeding the implant on boot. |
| 80 | + |
43 | 81 | ## Persistence & forensic tips |
44 | 82 | - Supply chain placement: malicious static lib `libVndxUtils.a` linked into `libandroid_runtime.so` during build (e.g., `vendor/mediatek/proprietary/external/libutils/arm[64]/libVndxUtils.a`). |
45 | 83 | - Firmware auditing: firmware images ship as Android Sparse `super.img`; use `lpunpack` (or similar) to extract partitions and inspect `libandroid_runtime.so` for extra calls in `println_native`. |
46 | 84 | - On-device artifacts: presence of `/data/dalvik-cache/arm*/system@framework@vndx_10x.jar@classes.jar`, logcat tag `AK_CPP`, or protected broadcasts named `com.action.SystemOptimizeService`/`com.action.SystemProtectService` indicate compromise. |
| 85 | +- For Android rootkits deployed from apps instead of factory supply chain, also inspect: |
| 86 | + - APK `assets/` for polyglot PNGs with appended data after `IEND` |
| 87 | + - Replaced `/system/lib*/libandroid_runtime.so` or `/system/lib*/libmedia_jni.so` |
| 88 | + - Modified framework oat/odex/vdex files that preserve hook redirections |
| 89 | + - Periodic watchdog processes that rewrite removed files or trigger forced reboots |
47 | 90 |
|
48 | 91 | ## References |
49 | 92 | - [Keenadu firmware backdoor analysis](https://securelist.com/keenadu-android-backdoor/118913/) |
50 | 93 | - [lpunpack utility for Android sparse images](https://github.com/unix3dgforce/lpunpack) |
| 94 | +- [Operation NoVoice: Rootkit Tells No Tales](https://www.mcafee.com/blogs/other-blogs/mcafee-labs/new-research-operation-novoice-rootkit-malware-android/) |
51 | 95 |
|
52 | 96 | {{#include ../../banners/hacktricks-training.md}} |
0 commit comments