Article
DHCSpy - Discovering the Iranian APT MuddyWater
Sep 29, 2025
Randorisec - Shindan
Authors: Paul (R3dy) Viard
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.
According to MITRE ATT&CK:
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.
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.
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.
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 Application
subclass 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.
The SplashActivity
, located in the com.p003bl.bl_vpn.activities
package, is defined as the MAIN
and LAUNCHER
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.
During this phase, the malware gathers sensitive device-specific information using getConfigRequestModel
.
For instance:
The data structure sent can be explain in 3 tables:
Request model
ConfigRequestClientInfoModel
- Basic Device Fingerprint
Field | Description |
---|---|
| Device model |
| Always "Android" |
| Android SDK version |
| Connection type: "WIFI" or "MOBILE_DATA" |
| Device timezone |
| System language |
ConfigRequestBodyModel
- Deep Sytem and Application Info
Field | Description |
---|---|
| The nested object above |
| Subscriber IDs (can reveal SIM country/operator) |
| SIM card info (could include carrier, slot status) |
| App's package ID (e.g., |
| App version installed (here 1.3.0) |
| App UI language |
| Possibly related to VPN configuration |
| Data usage counters |
| App/device uptime |
| VPN or service connected time |
| External IP address (via HTTP request) |
| Local IP address (via HTTP request) |
| Array of client IDs |
| Extra data if needed, usually empty |
ConfigRequestModel
- Root Request
Field | Description |
---|---|
| The full |
| Unique device ID (non-resettable unless factory reset) |
| Always "100", used by the C2 to distinguish request types |
| App or campaign-specific label |
| 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).
This base URL is obtained randomly between the two URL stocked inside com.p003bl.server_api.consts
:
Then, a POST request is sent to the randomly selected 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 data structure received is explained in tables below:
Response model
ovpnModel
- ovpn_list
Field | Description |
---|---|
| Name or label of the VPN |
| Base64-encoded |
| Possibly order of preference (0 = high?) |
dataModel
- data
Field | Description |
---|---|
| List of OpenVPN configuration objects |
| Possibly the identifier of a profile |
| Not set in this sample ( |
configResponseBodyModel
- body
Field | Description |
---|---|
| Server Mode: "error", "msg", "ovpn", "update" & "url" |
| The nested object above |
orderModel
- order
Field | Description |
---|---|
| Permissions and Commands code |
| Destination of the storage server (sftp) |
| Password for the archive containing the stolen data |
| Identifier for this "order" |
configResponseModel
Field | Description |
---|---|
| The nested object above |
| The nested object above |
JSON Response
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/
.
Using the configResponseModel
class, setOVPN
method reads and decodes the base64-encoded OpenVPN configuration (content
field inside ovpn_list
).
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.
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.
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).
For instance:
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.
Once all necessary permissions are approved, the VPN is prepared and started.
Start the VPN
The VPN is launched via the startVpn
and prepareVpn
methods, relying on the following manifest configuration, which registers a bound VPN service:
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:
If user consent is still required, the application launches the system-managed VPN consent dialog:
Once the user grants permission (or if permission is already available), the VPN connection is initialized through:
The VPN configuration sent by the C2 earlier is parsed using the ConfigParser
class and used to establish the VPN connection.
Permissions and Capabilities
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).
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 |
---|---|---|
| // | // |
| // | // |
| // | // |
| // | // |
| // | // |
| // | // |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
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.
For instance:
At the end, the malware uses SFTP (SSH File Transfer Protocol) to upload its files.
Inspecting the application logs during execution reveals critical information about DHCSpy’s infrastructure, specifically, credentials for accessing its secure File Transfer Protocol (SFTP) server.
This log is produced by a call to Log.d
in handleServerStates
:
Autostart on Xiaomi device
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.
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:
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.
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.
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
.
External IP
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.
Location
According to the ipapi documentation:
ipapi provides an easy-to-use API interface allowing customers to look various pieces of information IPv4 and IPv6 addresses are associated with.
Unknown Database
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.
Update feature
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:
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
.

IOCs
SHA256
a4913f52bd90add74b796852e2a1d9acb1d6ecffe359b5710c59c82af59483ec
48d1fd4ed521c9472d2b67e8e0698511cea2b4141a9632b89f26bd1d0f760e89
Files
/data/data/com.earth.earth_vpn/databases/dsbc.db
/data/data/com.earth.earth_vpn/databases/vsbc.db
Command and Control
hxxps://r1[.]earthvpn[.]org[:]3413/
hxxps://r2[.]earthvpn[.]org[:]3413/
hxxps://r1[.]earthvpn[.]org[:]1254/
hxxps://r2[.]earthvpn[.]org[:]1254/
hxxps://it1[.]comodo-vpn[.]com[:]1953
hxxps://it1[.]comodo-vpn[.]com[:]1950