NoVoice¶
NoVoice is an Android rootkit campaign discovered by McAfee in March 2026 that was distributed through 50+ apps on Google Play with at least 2.3 million downloads. The apps appeared as legitimate cleaners, games, and gallery utilities, but in the background they contacted a C2 server, profiled the device, and downloaded root exploits tailored to the device's specific hardware and software. On successful exploitation, NoVoice replaces core system libraries (libandroid_runtime.so, libmedia_jni.so) so that every app on the device runs attacker code at launch via the Zygote process. The infection survives factory reset and can only be removed by reflashing firmware.
The name comes from R.raw.novioce, a silent audio resource embedded in a later-stage payload, played at zero volume to keep a foreground service alive via Android's media playback exemption.
Overview¶
| Attribute | Details |
|---|---|
| First Seen | March 2026 |
| Status | Active (C2 infrastructure active at time of publication) |
| Type | Rootkit, plugin framework |
| Attribution | Linked to Triada.231 (shared os.config.ppgl.status property, same libandroid_runtime.so replacement technique) |
| Distribution | 50+ Google Play apps, 2.3M+ downloads |
| Target Region | Nigeria, Ethiopia, Algeria, India, Kenya (budget devices, older Android versions) |
| Exploited Vulnerabilities | Patched 2016-2021. Devices with security patch level 2021-05-01+ not susceptible to recovered exploits |
Distribution¶
All carrier apps were distributed through Google Play with no unusual permissions. Malicious components registered under com.facebook.utils, blending with the real Facebook SDK classes already present. No sideloading or user interaction beyond opening the app was required.
The initial payload is embedded in the app's asset directory as a polyglot image: a valid PNG that renders normally, with encrypted malicious data appended after the IEND marker (magic value CAFEBABE). Image viewers treat the file as a normal image since IEND signals the end of image data.
Most carrier apps check whether the device runs Android 12L or below before proceeding. A subset skips the version check entirely.
Multi-Stage Attack Chain¶
NoVoice operates as an 8-stage pipeline where each stage decrypts and loads the next. No single file contains the full chain, and most stages are delivered from the server at runtime.
Stage 1: Delivery¶
Code injected into the Facebook SDK initialization path runs automatically on app launch. It checks whether the device was already processed, extracts the encrypted payload from the polyglot image asset (enc.apk), decrypts it to produce h.apk, loads it into memory, and deletes all intermediate files. If the device was already processed, it cleans up files from previous runs and logs a fake Facebook SDK error: "FacebookSdk: Failed in initStore.".
Stage 2: Gatekeeper¶
h.apk loads a native library (libkwc.so) that verifies it is running inside the intended carrier app by checking the package name and signing certificate against hardcoded values. It contains two encrypted payloads:
sec.jar (gate): runs 15 anti-analysis checks before allowing the chain to continue:
| Check | Details |
|---|---|
| Emulator detection | Standard emulator indicators |
| Root indicators | su binary, root management apps |
| Debugger detection | JDWP, debug flags |
| VPN/proxy connections | Active VPN or proxy |
| Xposed hooks | Xposed framework artifacts |
| GPS geofencing | Excludes devices inside Beijing and Shenzhen bounding boxes (hardcoded coordinates) |
If the app lacks location permission, the geofence check defaults to allowing the chain to continue. Two brands receive special treatment: on Gionee devices, all checks except the geofence are skipped (Gionee has a documented history of shipping pre-installed malware). On Meizu devices, the chain follows a separate code path.
hex.jar: loaded only if all gate checks pass, begins contacting the C2 server.
Stage 3: Plugin Framework¶
hex.jar sets up a plugin framework built on a codebase the authors refer to as "kuwo" in their package names. It checks in with the C2 every 60 seconds. Plugin updates are delivered as image files with encrypted data after the image content. The server returns download URLs in a response field named warningIcon, disguising plugin downloads as icon fetches.
The first plugin delivered (rt) acts as an orchestrator. It manages sub-plugins and C2 communication, sending 30+ device identifiers including hardware model, kernel version, installed packages, and root status. This plugin contains the R.raw.novioce silent audio resource.
Stage 4: Exploitation¶
security.jar sends the device's chipset, kernel version, security patch date, and other identifiers to the C2. The server responds with exploit binaries matched to that specific device. McAfee recovered 22 exploits total. Before any exploit runs, the rootkit installer (CsKaitno.d) is decrypted from an embedded resource and written to disk.
One analyzed exploit chain uses three stages:
| Stage | Technique |
|---|---|
| Kernel read | IPv6 use-after-free vulnerability |
| Kernel read/write | Mali GPU driver vulnerability |
| Persistence | Credential patching + SELinux disablement |
The expected end result across all exploits: a root shell with SELinux disabled, which then loads CsKaitno.d.
Stage 5: Rootkit Installation¶
CsKaitno.d carries four encrypted payloads:
| Payload | Architecture | Purpose |
|---|---|---|
asbymol |
ARM32 | Library hook wrapper |
bdlomsd |
ARM64 | Library hook wrapper |
jkpatch |
N/A | Pre-compiled framework bytecode patcher |
watch_dog |
N/A | Persistence daemon |
The installer first removes files associated with competing rootkits. It then backs up the original libandroid_runtime.so and replaces it with a hook binary matched to the device architecture. It also replaces libmedia_jni.so. The replacements are wrappers that intercept the system's own functions and redirect to attacker code.
jkpatch provides a second persistence layer by modifying pre-compiled framework bytecode on disk. Even if someone restores the original library, the framework's compiled code still contains injected redirections.
Stage 6: Watchdog¶
The installer replaces the system crash handler with a rootkit launcher, installs recovery scripts, and stores a fallback copy of the exploitation stage on the system partition. The watch_dog daemon checks the installation every 60 seconds. If anything is missing, it reinstalls it. If reinstallation fails repeatedly, it forces a reboot, bringing the device back with the rootkit intact.
After cleanup, the installer marks the device as compromised by setting the system property os.config.ppgl.status.
Stage 7: Universal Injection¶
On boot, the Zygote process loads the replaced libandroid_runtime.so. Every app forked from Zygote inherits the attacker's code. Two payloads activate based on the host process:
| Payload | Target | Function |
|---|---|---|
| BufferA | Package installer | Silent app install/uninstall |
| BufferB | Any app with internet access | Primary post-exploitation tool, dual C2 channels |
Both payloads are embedded as fragments inside the replaced libandroid_runtime.so, assembled in memory at runtime, and deleted from disk immediately after loading. BufferB can be active in dozens of apps simultaneously on a single device.
BufferB operates two independent C2 channels with separate encryption keys and beacon intervals. If all primary domains fail and 3+ days pass without contact, a fallback routine activates between 1-4 AM, reaching out to api[.]googlserves[.]com for a fresh domain list.
Stage 8: Task Execution¶
The only recovered task payload is PtfLibc, delivered to BufferB from Alibaba Cloud OSS, targeting WhatsApp session cloning:
| Data Stolen | Details |
|---|---|
| Encryption database | WhatsApp's encrypted message database |
| Signal protocol identity keys | Device identity key pair |
| Registration ID | Signal protocol registration |
| Signed prekey | Most recent signed prekey |
| 12 local storage keys | Phone number, push name, country code, Google Drive backup account |
| Client keypair | Tries multiple decryption methods depending on key storage |
The stolen data enables the attacker to clone the victim's WhatsApp session onto another device. Data is sent to api[.]googlserves[.]com through multiple encryption layers. The framework is designed to accept any number of task payloads for any app at any time.
C2 Infrastructure¶
NoVoice separates its infrastructure by function, so taking down one domain does not affect others.
| Domain | Function |
|---|---|
fcm[.]androidlogs[.]com |
Initial device enrollment |
stat[.]upload-logs[.]com |
Primary C2: plugin delivery, device checkin, exploit distribution, result reporting |
config[.]updatesdk[.]com |
Fallback C2 |
download[.]androidlogs[.]com |
Exploit binary hosting |
logserves[.]s3-accelerate[.]amazonaws[.]com |
S3-accelerated CDN for exploits |
prod-log-oss-01[.]oss-ap-southeast-1[.]aliyuncs[.]com |
Alibaba Cloud OSS for task payloads (PtfLibc) |
api[.]googlserves[.]com |
BufferB C2, PtfLibc exfiltration, fallback domain list |
BufferB's domain lists can be updated at runtime. The C2 can push any payload to any app on the device.
Persistence¶
NoVoice achieves the most persistent infection model in documented Android malware:
| Mechanism | Details |
|---|---|
| System library replacement | libandroid_runtime.so and libmedia_jni.so replaced with hook wrappers |
| Framework bytecode patching | jkpatch modifies pre-compiled framework bytecode as a second persistence layer |
| Watchdog daemon | Checks installation every 60 seconds, reinstalls missing components, forces reboot on failure |
| Crash handler replacement | System crash handler replaced with rootkit launcher |
| Recovery scripts | Installed on system partition |
| Exploitation stage backup | Fallback copy stored on system partition |
| Factory reset survival | Writes to system partition, which factory reset does not wipe |
Remediation requires a full firmware reflash. Standard factory reset is insufficient.
IOCs¶
C2 Domains¶
| Domain |
|---|
api.googlserves[.]com |
api.uplogconfig[.]com |
avatar.ttaeae[.]com |
awslog.oss-accelerate.aliyuncs[.]com |
check.updateconfig[.]com |
config.googleslb[.]com |
config.updatesdk[.]com |
dnskn.googlesapi[.]com |
download.androidlogs[.]com |
fcm.androidlogs[.]com |
log.logupload[.]com |
logserves.s3-accelerate.amazonaws[.]com |
prod-log-oss-01.oss-ap-southeast-1.aliyuncs[.]com |
sao.ttbebe[.]com |
stat.upload-logs[.]com |
upload.crash-report[.]com |
nzxsxn.98kk89[.]com |
98kk89[.]com |
Carrier App Samples (Selection)¶
| Hash (SHA-256) | Package |
|---|---|
03e62ac5... |
com.filnishww.fluttbuber.storagecleaner |
066a096a... |
com.wififinder.wificonnect |
0751decd... |
com.wuniversal.lassistant |
106edd06... |
com.crazycodes.blendphoto |
4830a985... |
com.khanbro.gamestation |
98819230... |
com.crazycodes.airvpn |
a430123e... |
com.gbversion.gbplus.gblatestversion |
fd62c2bf... |
com.game.ludoplay |
50+ carrier apps identified in total. Full hash list available in McAfee's report.
Related Families¶
NoVoice shares multiple indicators with Triada.231: the os.config.ppgl.status system property for tracking installation state, persistence via libandroid_runtime.so replacement, and Zygote injection so every app runs attacker code. Whether NoVoice is a direct evolution of Triada.231, a fork, or a separate group reusing proven techniques, the shared approach suggests access to a common toolchain.
Keenadu also compromises libandroid_runtime.so for universal app injection and shares supply chain/firmware-level persistence characteristics, though Keenadu arrives pre-installed rather than through exploitation. Both NoVoice and Keenadu represent the convergence of Play Store distribution and firmware-level persistence that was previously exclusive to supply chain attacks.
The plugin-based architecture with runtime-delivered task payloads is similar to LightSpy's modular framework, though NoVoice operates at a deeper system level. The WhatsApp session cloning capability is specific to NoVoice and not documented in other Android malware families at this depth (Signal protocol key extraction, signed prekey theft, multi-method client keypair decryption).