Skip to content

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.

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).

References