In this article, we will deep dive into internals works and key components of a new sample of the DHCSpy Android spyware family, discovered by Lookout after the start of the Israel-Iran conflict. This malware is developed and maintained by an Iranian APT : MuddyWater.
MuddyWater is a cyber espionage group assessed to be a subordinate element within Iran's Ministry of Intelligence and Security (MOIS).
A potential developer identifier was found after analyzing the compilation traces in the various libraries of the APK : "hossein" We were able to recover a sample of DHCSpy named Earth VPN. It was directly downloaded directly from this URL : hxxps://www[.]earthvpn[.]org, which is now down.
Overview
DHCSpy is a malicious spyware disguised as a VPN application, built on edited open-source OpenVPN code. This design allows it to automatically run whenever the victim activates the VPN. Once active, the malware operates in the background, secretly collecting sensitive data such as WhatsApp files, contact lists, videos, and more.
DHCSpy was first discovered by Lookout on July 16, 2023. At the time of discovery, the malware was identified as Hide VPN. Subsequently, multiple variants from the same spyware family emerged, including Hazrat Eshq, Earth VPN, and Comodo VPN.
During the analyze of a sample of Comodo VPN, traces of an old test response from a command server indicated that the malware has been in development since August 10, 2022.
According to the Lookout report, the earliest known Earth VPN sample was obtained on July 20 2025, although archived snapshots from the Wayback Machine indicate that its distribution site had been active as early as March 2024.
Understanding the Manifest
Package & SDK Targeting
In the manifest file, the `versionName` of the EarthVPN application is set to "1.3.0", with a versionCode of 4. Its package name, com.earth.earth_vpn, using a common VPN-related naming conventions, suggest an attempt to impersonate a real VPN application.
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="4"
android:versionName="1.3.0"
android:compileSdkVersion="33"
android:compileSdkVersionCodename="13"package="com.earth.earth_vpn"platformBuildVersionCode="33"platformBuildVersionName="13">
<uses-sdkandroid:minSdkVersion="22"android:targetSdkVersion="26"/>
<!-- ...
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="4"
android:versionName="1.3.0"
android:compileSdkVersion="33"
android:compileSdkVersionCodename="13"package="com.earth.earth_vpn"platformBuildVersionCode="33"platformBuildVersionName="13">
<uses-sdkandroid:minSdkVersion="22"android:targetSdkVersion="26"/>
<!-- ...
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="4"
android:versionName="1.3.0"
android:compileSdkVersion="33"
android:compileSdkVersionCodename="13"package="com.earth.earth_vpn"platformBuildVersionCode="33"platformBuildVersionName="13">
<uses-sdkandroid:minSdkVersion="22"android:targetSdkVersion="26"/>
<!-- ...
Next, to understand the capabilities of DHCSpy, we need to examine the permissions it requests.
Requested Permissions
The purpose of DHCSpy is to steal various sensitive information from the device and its user. It is therefore necessary to acquire several authorizations to access this information.
This malware is still in development as we will see at the end of the article. Certain legit permissions, such as REQUEST_INSTALL_PACKAGES, can be used to download a malware update in order to add capabilities.
xml
<!-- ... -->
<uses-permissionandroid:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<
xml
<!-- ... -->
<uses-permissionandroid:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<
xml
<!-- ... -->
<uses-permissionandroid:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<
Due to its nature, certain permissions are commonly used to ensure the VPN stays active and the malicious behavior continues, such as POST_NOTIFICATIONS that lets application show notifications to the user. RECEIVE_BOOT_COMPLETED allows the application to start a background process or service automatically after the device finishes booting and WAKE_LOCK keep the CPU awake even when the screen is off.
xml
<!-- ... -->
<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permissionandroid:name="android.permission.WAKE_LOCK"/>
<!-- ...
xml
<!-- ... -->
<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permissionandroid:name="android.permission.WAKE_LOCK"/>
<!-- ...
xml
<!-- ... -->
<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permissionandroid:name="android.permission.WAKE_LOCK"/>
<!-- ...
Finally, we will examine the application’s declared components, including its activities, as defined in the manifest file. This analysis will help identifying potential entry points, UI decoys, and behavior triggers used by the fake VPN application.
Application & Activities
The fully qualified name of the Applicationsubclass is "de.blinkt.openvpn.core.ICSOpenVPNApplication". This class is initialized during the app startup phase, before any other components are created.
The package name correspond to a open source implementation of OpenVPN for android.
The source code is availble here : https://github.com/schwabe/ics-openvpn/tree/master
According to the github page project:
With the new VPNService of Android API level 14+ (Ice Cream Sandwich) it is possible to create a VPN service that does not need root access. This project is a port of OpenVPN.
This serves as an initial entry point to perform early-stage tasks to setup OpenVPN.
xml
<application
...
android:name="de.blinkt.openvpn.core.ICSOpenVPNApplication"
xml
<application
...
android:name="de.blinkt.openvpn.core.ICSOpenVPNApplication"
xml
<application
...
android:name="de.blinkt.openvpn.core.ICSOpenVPNApplication"
The SplashActivity, located in the com.p003bl.bl_vpn.activities package, is defined as the MAIN and LAUNCHER activity.
xml
<activity
android:name="com.p003bl.bl_vpn.activities.SplashActivity"
android:screenOrientation="portrait">
<intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity>
xml
<activity
android:name="com.p003bl.bl_vpn.activities.SplashActivity"
android:screenOrientation="portrait">
<intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity>
xml
<activity
android:name="com.p003bl.bl_vpn.activities.SplashActivity"
android:screenOrientation="portrait">
<intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity>
Another feature that can be abused by this Android malware is Deep Linking.
The exported activity com.p003bl.bl_vpn.activities.MainActivity is configured to intercept browsable HTTPS links to https://www.google.com/* via an intent filter.
This technique can be used in a malicious way to steal user information.Here an example from research by Lauritz and kun_19.
As a result, in case the end-user selects the malicious app, the sensitive OAuth credentials are sent to the malicious app.
In this particular version of the malware, this feature is not utilized, as it will be demonstrated in the subsequent analysis.
In the following section, First Launch, we will analyze EarthVPN's behavior during its initial execution and uncover the techniques it uses to establish its VPN connection.
First Launch
On its first execution, the DHCSpy sample immediately carries out a series of initialization steps, both to configure its VPN component and to prepare for systematic user data theft. The following section breaks down these preparatory actions, revealing the underlying logic and techniques used by the malware authors.
Internal VPN Service
Inside BaseActivity, It can be noted that the malware exhibits different behaviors on Xiaomi devices, as we will see in section XIAOMI PART.
The method initServiceConnection is used to talk to a background VPN service using AIDL (Android Interface Definition Language), which allows the application and the service to exchange information even if they run in separate processes.
To function properly, a VPN application requires two mandatory initialization steps. Firstly, it establishes a connection with its internal VPN service, which starts and binds to the background process that maintains the VPN tunnel.Secondly, the VPN Configuration defines how the VPN should connect (server, credentials, protocol, routes, etc.).
This second steps will be discussed later in the article. First of all, to obtain the VPN configuration, the malware must contact its C2 using the checkVpnState method, which is subsequently called in serviceConnected.
Request to C2
Following the initialization, the next relevant step is the invocation of the init method inside checkVpnState.This last method determines the VPN status and either proceeds to the main activity if an active VPN session is detected, or initiates a connection sequence.
The init() method sets up UI elements for a loading state and triggers a configuration request through ConfigRepository.getConfig(). This request includes parameters such as command ID and connection time.
ConfigRequestBodyModel - Deep Sytem and Application Info
Field
Description
client_info
The nested object above
IMSI_1 / IMSI_2
Subscriber IDs (can reveal SIM country/operator)
SIM_1 / SIM_2
SIM card info (could include carrier, slot status)
package_name
App's package ID (e.g., com.earth.earth_vpn)
app_version
App version installed (here 1.3.0)
language
App UI language
ovpn_id
Possibly related to VPN configuration
inputByteCount / outputByteCount
Data usage counters
upTime
App/device uptime
connectedTime
VPN or service connected time
publicIP
External IP address (via HTTP request)
privateIP
Local IP address (via HTTP request)
ids
Array of client IDs
data
Extra data if needed, usually empty
ConfigRequestModel - Root Request
Field
Description
body
The full ConfigRequestBodyModel
android_id
Unique device ID (non-resettable unless factory reset)
request_code
Always "100", used by the C2 to distinguish request types
label
App or campaign-specific label
date
Timestamp of the request in ISO 8601 format
POST request
This data is then sent using Retrofit, a type-safe HTTP client for Android. It simplifies communication with REST APIs by turning HTTP request into Java method calls. A method getRetofit creates and returns a singleton Retrofit instance configured with a base URL (the C2 configuration server).
After sending the configuration request, the malware waits for a response from the C2 server. This response contains key parameters needed to configure and initiate the VPN, as well as other operational instructions used to control the application behavior.
Response from C2
Directly after receiving a response, the isServerDataReceived method extracts a configResponseModel used to create the VPN profile and prepare the malware behavior.
The JSON response includes a mode field set to "ovpn", indicating to the malware that the configuration data is located within the content field. The malware then parses this VPN payload, validates its parameters, and builds a temporary VPN profile, which is subsequently used to initiate the tunnel.
VPN Configuration
The order field in the JSON response contains four mandatory pieces of information. These values are then written into a database file named dsbc.db, located in /data/data/com.earth.earth_vpn/databases/.
The sub-method makeServer retrieves the decoded OpenVPN configuration string and parses key parameters like ip, port and country to populate a Server object. It should be noted that all recovered information are logged on the device.
javapublic Server makeServer(String ovpnContent,String emptyString)throws IOException {/* ...*/while(true){String line = content.readLine();if(line != null){if(line.startsWith("remote ")){Log.i("SERVER_TYPE","server type: ip and port");String[]strArrSplit = line.split(" ");server.setIp(strArrSplit[1]);server.setPort(strArrSplit[2]);}elseif(line.startsWith("cipher ")){Log.i("SERVER_TYPE","server type: cipher key");server.setCipher(line.split(" ")[1]);}elseif(line.startsWith("# country")){Log.i("SERVER_TYPE","server type: country name");server.setCountry(line.split(" ")[2]);}byteArrayOutputStream.write(line.getBytes(),0,line.getBytes().length);byteArrayOutputStream.write("\n".getBytes());}else{server.setContent(ovpnContent);server.setFileName(emptyString);Base64.encode(byteArrayOutputStream.toByteArray(),0);returnserver;}}}
javapublic Server makeServer(String ovpnContent,String emptyString)throws IOException {/* ...*/while(true){String line = content.readLine();if(line != null){if(line.startsWith("remote ")){Log.i("SERVER_TYPE","server type: ip and port");String[]strArrSplit = line.split(" ");server.setIp(strArrSplit[1]);server.setPort(strArrSplit[2]);}elseif(line.startsWith("cipher ")){Log.i("SERVER_TYPE","server type: cipher key");server.setCipher(line.split(" ")[1]);}elseif(line.startsWith("# country")){Log.i("SERVER_TYPE","server type: country name");server.setCountry(line.split(" ")[2]);}byteArrayOutputStream.write(line.getBytes(),0,line.getBytes().length);byteArrayOutputStream.write("\n".getBytes());}else{server.setContent(ovpnContent);server.setFileName(emptyString);Base64.encode(byteArrayOutputStream.toByteArray(),0);returnserver;}}}
javapublic Server makeServer(String ovpnContent,String emptyString)throws IOException {/* ...*/while(true){String line = content.readLine();if(line != null){if(line.startsWith("remote ")){Log.i("SERVER_TYPE","server type: ip and port");String[]strArrSplit = line.split(" ");server.setIp(strArrSplit[1]);server.setPort(strArrSplit[2]);}elseif(line.startsWith("cipher ")){Log.i("SERVER_TYPE","server type: cipher key");server.setCipher(line.split(" ")[1]);}elseif(line.startsWith("# country")){Log.i("SERVER_TYPE","server type: country name");server.setCountry(line.split(" ")[2]);}byteArrayOutputStream.write(line.getBytes(),0,line.getBytes().length);byteArrayOutputStream.write("\n".getBytes());}else{server.setContent(ovpnContent);server.setFileName(emptyString);Base64.encode(byteArrayOutputStream.toByteArray(),0);returnserver;}}}
The returned server object is then passed to intentMainActivity, which triggers the onCreate method of MainActivity.class. This activity stores the configuration and subsequently calls startVpn during the VPN startup phase.
Using the code field received in the C2 response and then stocked inside a database, the malware dynamically determines which permissions it has to request from the user. These permissions are essential to enable further malicious capabilities, such as accessing sensitive data or interacting with system components.
Runtime Permissions
The RequestMultiplePermissions class is an Android ActivityResultContract used to request multiple runtime permissions from the user and return a map of each permission to a Boolean indicating whether it is granted (true) or denied (false). The onCreate method of MainActivity class retrieves an ImageView component, which visually represents the VPN's power or toggle button. It assigns this view to the powerIcon class field for further reference. A click listener is attached to this button. When the user taps it, the buttonPowerClick(View view) method is invoked. This function likely initiates or toggles the VPN connection logic, providing users with intuitive control over their secure connection status.
javaprotected voidonCreate(Bundle bundle){/*
Get the field of the server
Ask for runtime permissions */this.requestPermissionListLauncher = registerForActivityResult(newActivityResultContracts.RequestMultiplePermissions(),newActivityResultCallback(){
@Override// androidx.activity.result.ActivityResultCallback public final voidonActivityResult(Object obj){this.f$0.switchVPN((Map) obj);}});// Wait for a click on the power buttonImageView imageView = (ImageView)findViewById(C0686R.id.powerImage);this.powerIcon = imageView;imageView.setOnClickListener(newView.OnClickListener(){
@Overridepublic final voidonClick(View view){this.f$0.wrpPowerClick(view);}});/* ... */
javaprotected voidonCreate(Bundle bundle){/*
Get the field of the server
Ask for runtime permissions */this.requestPermissionListLauncher = registerForActivityResult(newActivityResultContracts.RequestMultiplePermissions(),newActivityResultCallback(){
@Override// androidx.activity.result.ActivityResultCallback public final voidonActivityResult(Object obj){this.f$0.switchVPN((Map) obj);}});// Wait for a click on the power buttonImageView imageView = (ImageView)findViewById(C0686R.id.powerImage);this.powerIcon = imageView;imageView.setOnClickListener(newView.OnClickListener(){
@Overridepublic final voidonClick(View view){this.f$0.wrpPowerClick(view);}});/* ... */
javaprotected voidonCreate(Bundle bundle){/*
Get the field of the server
Ask for runtime permissions */this.requestPermissionListLauncher = registerForActivityResult(newActivityResultContracts.RequestMultiplePermissions(),newActivityResultCallback(){
@Override// androidx.activity.result.ActivityResultCallback public final voidonActivityResult(Object obj){this.f$0.switchVPN((Map) obj);}});// Wait for a click on the power buttonImageView imageView = (ImageView)findViewById(C0686R.id.powerImage);this.powerIcon = imageView;imageView.setOnClickListener(newView.OnClickListener(){
@Overridepublic final voidonClick(View view){this.f$0.wrpPowerClick(view);}});/* ... */
When the victim hit the powerIcon, powerClick is executed. The command code is retrieved from the previously created database dsbc.db and used inside PermissionUtil.getPermissionList.This code is a string of 16 characters which can be either 1 or 0.
This method interprets the last 10 characters of a string (str) to determine which Android permissions should be requested. Each character corresponds to a specific permission (or set of permissions).
A correlation between the permissions requested and the capabilities of the application is available in section Permissions and Capabilities. Then, the permissions list in strArr is transferred to checkRuntimePermissions.This method checks permissions at runtime and requests any that have not yet been granted.
This service is a subclass of android.net.VpnService, enabling the application to create a VPN interface. Firstly, the malware checks whether VPN permissions have already been granted by invoking:
The core of the program resides in the modified OpenVPN package de.blinkt.openvpn.core. Several functions have been added to integrate data theft capabilities into the VPN. For example, the runData method uses the previously discussed command code, which contains the various permissions requested from the user, not only to request those permissions but also to trigger specific actions on the device. In the snippet below, the same mechanism as in Runtime Permissions section is used to browse backwards the code string (renamed bitfield in the code below).
javaprivate voidrunData(final Stringbitfield,final String pass)throws Throwable {StringBuilder sb;int i = 0;try{try{try{if(bitfield.charAt(bitfield.length() - 1) == '1'){this.commandCounter.incrementAndGet();final String string = Integer.toString(0);getEmitterExecutor().schedule(newRunnable(){
@Override// java.lang.Runnable public final voidrun(){this.f$0.lambda$runData$5(string,bitfield,pass);}},100L,TimeUnit.MILLISECONDS);/* ... */
javaprivate voidrunData(final Stringbitfield,final String pass)throws Throwable {StringBuilder sb;int i = 0;try{try{try{if(bitfield.charAt(bitfield.length() - 1) == '1'){this.commandCounter.incrementAndGet();final String string = Integer.toString(0);getEmitterExecutor().schedule(newRunnable(){
@Override// java.lang.Runnable public final voidrun(){this.f$0.lambda$runData$5(string,bitfield,pass);}},100L,TimeUnit.MILLISECONDS);/* ... */
javaprivate voidrunData(final Stringbitfield,final String pass)throws Throwable {StringBuilder sb;int i = 0;try{try{try{if(bitfield.charAt(bitfield.length() - 1) == '1'){this.commandCounter.incrementAndGet();final String string = Integer.toString(0);getEmitterExecutor().schedule(newRunnable(){
@Override// java.lang.Runnable public final voidrun(){this.f$0.lambda$runData$5(string,bitfield,pass);}},100L,TimeUnit.MILLISECONDS);/* ... */
For instance, when triggered, lambda$runData$5 invokes a method from another class to execute the data theft routine:
By analyzing the functions invoked within runData, a correlation can be established between the permissions requested and the capabilities of the application. This mapping is detailed in the table below:
Bit Position
Permission
Running Function
1(LSB)
//
//
2
//
//
3
//
//
4
//
//
5
//
//
6
//
//
7
READ_EXTERNAL_STORAGE
Download- getFile
8
READ_EXTERNAL_STORAGE
Recordings- getFile
9
READ_EXTERNAL_STORAGE
Camera- getFile
10
READ_EXTERNAL_STORAGE
ScreenShots - getFile
11
READ_EXTERNAL_STORAGE
WhatsApp - getFile
12
getAppList
13
GET_ACCOUNTS
getAccount
14
READ_CALL_LOG
getCallog
15
READ_CONTACTS
getContact
16(MSB)
READ_PHONE_STATE(SDK>= 33 : READ_PHONE_NUMBERS)
getClientInfo
The package name com.matrix.ctor handles function calls. Each folder contains a class that retrieves valuable files on the devices, compresses it into a ZIP archive secured with a password.
For example, the WhatsAppFile class, invoked via whatsAppFile.getFile, searches multiple paths to locate the encrypted WhatsApp conversation database. The class defines constants for different storage locations, including the standard WhatsApp database (msgstore.db.crypt14) and the WhatsApp Business variant (msgstore.db.crypt14 in com.whatsapp.w4b).
When the files are recovered and zipped, a callback is triggered to transfer the archive to the extraction routine.
Exfiltration
The OpenVPNService class acts as the central controller, implementing the various callback interfaces corresponding to the application’s different capabilities (file theft, contact extraction, account enumeration). When a piece of information is successfully retrieved, such as a file, a contact list, or other targeted data, the responsible class calls its sendFinish method. This method, which appears in multiple capability-specific classes, serves as a generic way to signal that the data collection process is complete. sendFinish then invokes the appropriate callback method implemented by OpenVPNService, effectively passing the stolen data back to the main service for further processing or exfiltration.
Inspecting the application logs during execution reveals critical information about DHCSpy’s infrastructure, specifically, credentials for accessing its secure File Transfer Protocol (SFTP) server.
As noted earlier, the malware’s startup behavior varies depending on the device brand. This variation may be linked to market trends, as shown in the graph below, which highlights that in 2025, Xiaomi devices ranked second in sales in Iran.
On Xiaomi’s MIUI firmware, applications are by default prevented from registering for the BOOT_COMPLETED broadcast (and similar startup hooks) unless the user explicitly “whitelists” them in settings. That’s not an Android standard runtime permission, but a MIUI‐only toggle under “Autostart”.
In the BaseActivity class, during creation, the method showAutoStartPermissionDialog is called to check whether the Autostart permission is enabled for the application:
The method Autostart.INSTANCE.getAutoStartState(context) refers to a utility package created by Kumaraswamy, named MIUI-Autostart (xyz.kumaraswamy.autostart). According to the github page, MIUI-Autostart is:
A library to check MIUI autostart permission state.
MIUI’s autostart flag resides in private, non-SDK APIs:
These APIs are not publicly available in the standard Android SDK.
Starting with Android 9 (API level 28), Google began enforcing restrictions that block reflection-based access to non-SDK interfaces. To interact with MIUI’s internal autostart APIs, the autostart package relies on the AndroidHiddenApiBypass library. This library relies mainly on the Unsafe API. This is a very insecure class that allow developers to read and write memory in pure Java.
Using reflection, the developers can call Unsafe and use that instance to locate ART (Android Runtime) hidden API policy field and modify it.
javaHiddenApiBypass.addHiddenApiExemptions("");
javaHiddenApiBypass.addHiddenApiExemptions("");
javaHiddenApiBypass.addHiddenApiExemptions("");
This call informs the system to disable filtering entirely, allowing access to all non-SDK methods (since the empty-string prefix matches everything).
Here’s how it’s used in the library:
javapackage xyz.kumaraswamy.autostart;/* ... */static {if(Build.VERSION.SDK_INT >= 28){try{HiddenApiBypass.addHiddenApiExemptions("");}catch(Exception unused){Log.d(TAG,"Failed to bypass API Exemption");}}}
javapackage xyz.kumaraswamy.autostart;/* ... */static {if(Build.VERSION.SDK_INT >= 28){try{HiddenApiBypass.addHiddenApiExemptions("");}catch(Exception unused){Log.d(TAG,"Failed to bypass API Exemption");}}}
javapackage xyz.kumaraswamy.autostart;/* ... */static {if(Build.VERSION.SDK_INT >= 28){try{HiddenApiBypass.addHiddenApiExemptions("");}catch(Exception unused){Log.d(TAG,"Failed to bypass API Exemption");}}}
When this static block is executed, the execution flow proceeds to the getAutoStartState method called previously in BaseActivity. This method searches the android.miui.AppOpsUtils class to invoke the getApplicationAutoStart method and retrieve the actual state of the permission.
javapublic final State getAutoStartState(Context context)throws ... {/* ... */ObjectobjMethodGetState = fun_getApplicationAutoStart.invoke(null,context,context.getPackageName());Integer num = objInvokeinstanceofInteger ? (Integer) objInvoke : null;if(num == null){returnState.UNEXPECTED_RESULT;}int iIntValue = num.intValue();if(iIntValue == 0){returnState.ENABLED;}if(iIntValue == 1){returnState.DISABLED;}returnState.UNEXPECTED_RESULT;}
javapublic final State getAutoStartState(Context context)throws ... {/* ... */ObjectobjMethodGetState = fun_getApplicationAutoStart.invoke(null,context,context.getPackageName());Integer num = objInvokeinstanceofInteger ? (Integer) objInvoke : null;if(num == null){returnState.UNEXPECTED_RESULT;}int iIntValue = num.intValue();if(iIntValue == 0){returnState.ENABLED;}if(iIntValue == 1){returnState.DISABLED;}returnState.UNEXPECTED_RESULT;}
javapublic final State getAutoStartState(Context context)throws ... {/* ... */ObjectobjMethodGetState = fun_getApplicationAutoStart.invoke(null,context,context.getPackageName());Integer num = objInvokeinstanceofInteger ? (Integer) objInvoke : null;if(num == null){returnState.UNEXPECTED_RESULT;}int iIntValue = num.intValue();if(iIntValue == 0){returnState.ENABLED;}if(iIntValue == 1){returnState.DISABLED;}returnState.UNEXPECTED_RESULT;}
Finally, when the Autostart permission is not granted, the application displays an alert dialog that redirects the user to the MIUI Security Center to enable it.
javapublic static boolean showAutoStartPermissionDialog(final Contextcontext){if(!Build.MANUFACTURER.equalsIgnoreCase(Utils.BRAND_XIAOMI) || Autostart.INSTANCE.getAutoStartState(context) != Autostart.State.DISABLED){returnfalse;}AlertDialog alertDialogShow = newAlertDialog.Builder(newContextThemeWrapper(context,C0686R.style.AlertDialogPermission))
.setTitle(C0686R.string.autoStartPermission)
.setMessage("Autostart access is required for the program to work properly, otherwise the program will have problems.")
.setPositiveButton(C0686R.string.goToSettings,newDialogInterface.OnClickListener(){
@Override// android.content.DialogInterface.OnClickListener public voidonClick(DialogInterface dialogInterface,int i){Intent intent = newIntent();intent.setComponent(newComponentName("com.miui.securitycenter","com.miui.permcenter.autostart.AutoStartManagementActivity"));((Activity) context).startActivityForResult(intent,PointerIconCompat.TYPE_HAND);}}).setIcon(R.drawable.ic_dialog_alert).show();
javapublic static boolean showAutoStartPermissionDialog(final Contextcontext){if(!Build.MANUFACTURER.equalsIgnoreCase(Utils.BRAND_XIAOMI) || Autostart.INSTANCE.getAutoStartState(context) != Autostart.State.DISABLED){returnfalse;}AlertDialog alertDialogShow = newAlertDialog.Builder(newContextThemeWrapper(context,C0686R.style.AlertDialogPermission))
.setTitle(C0686R.string.autoStartPermission)
.setMessage("Autostart access is required for the program to work properly, otherwise the program will have problems.")
.setPositiveButton(C0686R.string.goToSettings,newDialogInterface.OnClickListener(){
@Override// android.content.DialogInterface.OnClickListener public voidonClick(DialogInterface dialogInterface,int i){Intent intent = newIntent();intent.setComponent(newComponentName("com.miui.securitycenter","com.miui.permcenter.autostart.AutoStartManagementActivity"));((Activity) context).startActivityForResult(intent,PointerIconCompat.TYPE_HAND);}}).setIcon(R.drawable.ic_dialog_alert).show();
javapublic static boolean showAutoStartPermissionDialog(final Contextcontext){if(!Build.MANUFACTURER.equalsIgnoreCase(Utils.BRAND_XIAOMI) || Autostart.INSTANCE.getAutoStartState(context) != Autostart.State.DISABLED){returnfalse;}AlertDialog alertDialogShow = newAlertDialog.Builder(newContextThemeWrapper(context,C0686R.style.AlertDialogPermission))
.setTitle(C0686R.string.autoStartPermission)
.setMessage("Autostart access is required for the program to work properly, otherwise the program will have problems.")
.setPositiveButton(C0686R.string.goToSettings,newDialogInterface.OnClickListener(){
@Override// android.content.DialogInterface.OnClickListener public voidonClick(DialogInterface dialogInterface,int i){Intent intent = newIntent();intent.setComponent(newComponentName("com.miui.securitycenter","com.miui.permcenter.autostart.AutoStartManagementActivity"));((Activity) context).startActivityForResult(intent,PointerIconCompat.TYPE_HAND);}}).setIcon(R.drawable.ic_dialog_alert).show();
Under Development
In the Understanding the Manifest section, we identified a feature called _deep linking_. However, no evidence of this functionality is present in the MainActivity class. Throughout this analysis, we will examine several pieces of evidence indicating that the malware is still in development and not in its final form. To support this conclusion, we will analyze both unused (dead) code and the application’s update routine.
Missing Calls
In this section, we present a non-exhaustive list of methods within the application that appear to be unused, as they are never invoked along any execution path nor referenced by other routines in the codebase.
Connectivity Test
The ping function is the only method in the application that executes a system command. In this case, it performs a basic connectivity test to Google’s public DNS server by invoking /system/bin/ping.
This method retrieves the external IP address of the device by querying https://icanhazip.com. Malware authors often leverage this URL to identify the geographic location or network characteristics of the infected user.
vsbc.db is another database defined in the code but never created or used during the execution of the malware. It contains a single table, usage, with fields for inbound and outbound bytes (in_byte, out_byte) and a timestamp (t_stamp). The structure suggests it may have been intended to log network traffic statistics or track application usage over time, although this functionality remains inactive.
Earlier in the Response from C2 section, we examined the structure of the configResponseModel class and how it stores critical information received from the C2 server. Upon the spyware’s initial launch, the mode variable is set to "ovpn" to initiate the OpenVPN setup. During further inspection, we identified additional mode string constants within the com.p003bl.server_api.model package:
javapublic static final String SERVER_MODE_ERROR = "error";public static final String SERVER_MODE_MESSAGE = "msg";public static final String SERVER_MODE_OVPN = "ovpn";public static final String SERVER_MODE_UPDATE = "update";public static final String SERVER_MODE_URL = "url";
javapublic static final String SERVER_MODE_ERROR = "error";public static final String SERVER_MODE_MESSAGE = "msg";public static final String SERVER_MODE_OVPN = "ovpn";public static final String SERVER_MODE_UPDATE = "update";public static final String SERVER_MODE_URL = "url";
javapublic static final String SERVER_MODE_ERROR = "error";public static final String SERVER_MODE_MESSAGE = "msg";public static final String SERVER_MODE_OVPN = "ovpn";public static final String SERVER_MODE_UPDATE = "update";public static final String SERVER_MODE_URL = "url";
In this section, we will focus specifically on the server update mode. This flowchart shows the DHCSpy remote update mechanism, where the C2 server can instruct the application to download and install an APK (Catalog.apk). Upon receiving an “update” mode from the server, it displays a notification to the user that triggers either a download or direct installation via installApk.
Shindan est une application SaaS, mobile et desktop qui détecte les compromissions et vulnérabilités sur smartphones et tablettes, sans accès aux données personnelles. Obtenez un diagnostic rapide et précis pour protéger vos VIP et collaborateurs.
Shindan est une application SaaS, mobile et desktop qui détecte les compromissions et vulnérabilités sur smartphones et tablettes, sans accès aux données personnelles. Obtenez un diagnostic rapide et précis pour protéger vos VIP et collaborateurs.
Shindan est une application SaaS, mobile et desktop qui détecte les compromissions et vulnérabilités sur smartphones et tablettes, sans accès aux données personnelles. Obtenez un diagnostic rapide et précis pour protéger vos VIP et collaborateurs.