Android malware come in to its role for a couple of years. Now it caught our team’s attention because we’re going to have enough time after long weekend to analyze on something that could help the community a little bit. Let’s take some look into it.

McAiden found in the news that the application can be downloaded by visiting on the link https://cc2[.]lol.

Figure 1: Exported chat from victim’s device, credit to Medium – Narunc

After visiting the website, there is a page for downloading 2 applications for iOS and Android platform. Anyway, we can only download the Android app. The iOS link seems do not work.

Figure 2: The website for downloading the app

Application Structure

By statically reviewing the application, here is its structure:

Figure 3: Application structure

After decompiling the application using jadx (https://github.com/skylot/jadx) some files caught our attention. There is a file named “libjiagu.so” which is used for protecting an APK file from being reverse engineered easily by packing the app logics and extract it at runtime. Another one is “easyagent” file which is later we discovered that it is an APK file. The last one is “tg.iapk” which is a ZIP and password protected and encrypted APK file.

AndroidManifest.xml

The application needs to declare some permissions and application attributes in the AndroidManifest.xml file. The following table describes the identifiable dangerous permissions requested by the app:

Permission RequestedDescription
android.permission.CALL_PHONEAllows the application to call phone without user interaction.
android.permission.PROCESS_OUTGOING_CALLSAllows the application to alter outgoing call e.g. change out going call number or even drop or redirect the call.
android.permission.GET_TASKSAllows the application to retrieve currently running tasks.
android.permission.MOUNT_UNMOUNT_FILESYSTEMSAllows the application to mount and unmount file systems.
android.permission.READ_SMSAllows the application to read SMS e.g. OTP
android.permission.SEND_SMSAllows the application to send an SMS message.
android.permission.RECORD_AUDIOAllows the application to record audio.
android.permission.REQUEST_INSTALL_PACKAGESAllows the application to request the user for installing another package.
android.permission.WRITE_APN_SETTINGSAllows the application to modify cellular APN.
android.permission.WRITE_SETTINGSAllows the application to modify the device settings.
android.permission.INJECT_EVENTSAllows the application to send input events (screen tap etc.) to another application.
android.permission.SYSTEM_ALERT_WINDOWAllows the application to create a window on top of other activities. (lead to overlay attack)
android.permission.BIND_ACCESSIBILITY_SERVICEAllows the application to access content of the running application (including other applications)
Table 2: Example of dangerous permissions requested by the app

There are still many other permissions requested by the app. Most of them are unnecessary for normal application and can cause harm in many ways.

File: tg.iapk

The following table describes the file information:

File nametg.iapk
File signature634e70a18127375f47c7b40564142ca5597640056a1745b701f7cec3c7701ff1
File Size2 MB
File TypeZIP & Password Protected & Encrypted
Table 3: tg.iapk file information

Decrypting tg.iapk

After trying to extract the file tg.iapk, it was found that the file was achieved with password.

Figure 4: Cannot extract the ZIP file

The password was found in the ZIP comment which is the following text:

Archive:  tg.iapk
1@386662363963333636636566653438373535363939303135383730373632356332316564636239302d356463312d343737322d623936352d396361653239613234363637
   creating: tg.iapk_unzip/com/plugin/
   creating: tg.iapk_unzip/com/plugin/tePlugin/
[tg.iapk] com/plugin/tePlugin/activityUtil.java password:

By extracting the hex value and decode it, the following text was obtained (the actual ZIP password):

$ python
>>> hexStr = "386662363963333636636566653438373535363939303135383730373632356332316564636239302d356463312d343737322d623936352d396361653239613234363637"
>>> bytearray.fromhex(hexStr).decode()
'8fb69c366cefe487556990158707625c21edcb90-5dc1-4772-b965-9cae29a24667'
>>>

After extracting the tg.iapk, it was found that the files in it are encrypted and cannot be read easily.

$ file tg.iapk_unzip/com/plugin/tePlugin/activityUtil.java 
tg.iapk_unzip/com/plugin/tePlugin/activityUtil.java: data
                                                                                                                                                                                                       
$ strings tg.iapk_unzip/com/plugin/tePlugin/activityUtil.java 
3864303638303136376538646535616239316235373665633534323030363465313637323739363638313330357c3132333435367c313637323739363638313330357c31353239
VTC^A^CNbC^[
]VAVGVT\VPR
G[BP^Y
CRg[BP^Y
:=^ZGXEC
[...]

$ hd -C tg.iapk_unzip/com/plugin/tePlugin/activityUtil.java
00000000  00 09 23 83 00 00 00 8e  33 38 36 34 33 30 33 36  |..#.....38643036|
00000000  00 09 23 83 00 00 00 8e  33 38 36 34 33 30 33 36  |..#.....38643036|
00000010  33 38 33 30 33 31 33 36  33 37 36 35 33 38 36 34  |3830313637653864|
00000010  33 38 33 30 33 31 33 36  33 37 36 35 33 38 36 34  |3830313637653864|
00000020  36 35 33 35 36 31 36 32  33 39 33 31 36 32 33 35  |6535616239316235|
00000020  36 35 33 35 36 31 36 32  33 39 33 31 36 32 33 35  |6535616239316235|
00000030  33 37 33 36 36 35 36 33  33 35 33 34 33 32 33 30  |3736656335343230|
00000030  33 37 33 36 36 35 36 33  33 35 33 34 33 32 33 30  |3736656335343230|
[...]

The following figure shows an example of an entire encrypted file (update.json):

Figure 6: Example of encrypted file content

After performing cryptanalysis on the encrypted file, we managed to find a way to discover the encryption key. The file was encrypted using XOR algorithm. Different files are encrypted with different keys. To discover the key for each file, we XOR the encrypted filename section with the actual filename then the key is disclosed. Here shows the file structure:

4 bytes [file magic number]
4 bytes [file header length]
N bytes [file header]
4 bytes [file name length]
[don’t care]
N bytes [file name]
N bytes [file content]
Table 4: Encrypted file structure

To demonstrate, we use the encrypted file “update.json” which has its hex content as follows:

File: update.json

00092383 0000008A 33303336 33323339 33303331 33363337 33313335 36323335 33323633 33303632 33313336 36333331 33333632 36323635 33353633 36363633 36363632 33313634 33313336 33373332 33373339 33363336 33383331 33363330 33323763 33313332 33333334 33353336 37633331 33363337 33323337 33393336 33363338 33313336 33303332 37633334 33340000 000B0000 002C181D 090C1908 43071E02 0316674D 4D4F181D 090C1908 32181F01 4F574D4F 4F41674D 4D4F1B08 1F1E0402 034F574D 4F5F435C 435F4F67 10

File magic number: 00092383

File header length: 0000008A (138 bytes)

File header content: 33303336 33323339 33303331 33363337 33313335 36323335 33323633 33303632 33313336 36333331 33333632 36323635 33353633 36363633 36363632 33313634 33313336 33373332 33373339 33363336 33383331 33363330 33323763 33313332 33333334 33353336 37633331 33363337 33323337 33393336 33363338 33313336 33303332 37633334 3334

File header (decode as hex):

3036323930313637313562353263306231366331336262653563666366623164313637323739363638313630327c3132333435367c313637323739363638313630327c3434

// decode as hex again
0629016715b52c0b16c13bbe5cfcfb1d1672796681602|123456|1672796681602|44

the last decimal number indicates the file content length, in this case 44 bytes.

File name length: 000B0000 (11 bytes)

File name: 181D090C 19084307 1E0203

File content: 16674D4D 4F181D09 0C190832 181F014F 574D4F4F 41674D4D 4F1B081F 1E040203 4F574D4F 5F435C43 5F4F6710

The file name is “update.json” we can use it to XOR with the encrypted file name as follows:

Table 5: File encryption key discovery

The key used for encrypting the file is “m” or 0x6d. We can use it to decrypt the file content:

Table 6: File content decryption

A python file was created to facilitate decrypting the encrypted files:

#!/usr/bin/env python


# Author: McAiden Consulting Co., Ltd. (2023.01.23)
# To PoC how to decrypt files in tg.iapk
import sys
import binascii

def xor(b1, b2): # use xor for bytes
	result = b""
	for b1, b2 in zip(b1, b2):
		result += bytes([b1 ^ b2])
	return result


with open(sys.argv[1], 'rb') as f:
	fileName = sys.argv[1]
	data = f.read()
	fileMagic = binascii.hexlify(data[0:4]).decode("ascii")
	fileHeaderLengthStr = binascii.hexlify(data[4:8]).decode("ascii")
	fileHeaderLength = int(fileHeaderLengthStr,16)
	fileHeader = binascii.hexlify(data[8:8+fileHeaderLength])
	decodedFileHeader = bytes.fromhex(bytes.fromhex(fileHeader.decode("ascii")).decode("ascii")).decode("ascii")

	fileContentLength = int(str(decodedFileHeader).rsplit('|', 1)[-1],0)
	fileNameLength = len(fileName)
	encryptedFileName = data[(-1)*(fileNameLength + fileContentLength):len(data) - fileContentLength]
	fileContent = data[(-1)*fileContentLength:]

	print("[!] fileMagic: " + str(fileMagic))
	print("[!] fileHeaderLength: " + str(fileHeaderLengthStr) + ", " + str(fileHeaderLength))
	print("[!] fileHeader: " + str(fileHeader))
	print("[!] decodedFileHeader: " + str(decodedFileHeader))
	print("[!] fileContentLength: " + str(fileContentLength))
	print("[!] fileNameLength: " + str(fileNameLength))
	print("[!] encryptedFileName: " + str(binascii.hexlify(encryptedFileName)))
	print("[!] fileContent: " + str(binascii.hexlify(fileContent)))

	a = fileName.encode('utf-8')[0:1]
	b = encryptedFileName[0:1]
	print("[!] a: " + str(a))
	print("[!] b: " + str(b))
	
	c = xor(a,b)
	key = c
	print("[!] key: " + str(key))

	decryptedContent = b""
	for i in range(0,len(fileContent)):
		decryptedContent += xor(fileContent[i:i+1], key)

	try:
		newFileName = "decrypted_"+fileName
		f = open(newFileName, "wb")
		f.write(decryptedContent)
		f.close()
		print("[!] saved to file: " + newFileName)
	except Exception as e:
		raise e

Let decrypt a file:

Table 7: Running the python script to decrypt AES.java
Table 8: Sample of the decrypted AES.java

tg.iapk Structure

The following figure shows the tg.iapk file structure:

Table 9: tg.iapk file structure

There are multiple files interesting, we decrypt all of them begin with the main.jar.

The decrypted main.jar is a java class file. By decompiling using the following jadx command,

$ jadx --comments-level debug decrypted_main.jar -d decrypted_main_jadx

In this file we found some interesting classes. In main.jar we found that there is a declaration which contains MOBILE BANKING (9 apps) and CRYPTO WALLET (6 apps) package names in it.

Table 10: Sample of decrypted main.jar

There is a declaration of package names indicating it is targeting crypto wallet apps, Thai (7 apps) and Singaporean (2 apps) banking apps. This does not mean the app in the list have any particular weakness or vulnerability, it just a list which the app focuses.

Overlay Attack

During analyzing the app, the app has requested SYSTEM_ALERT_WINDOW permission, if it is approved, the application will be able to create a window with TYPE_APPLICATION_OVERLAY. This means, the app can place an object or objects on a window on top of other activities but not above critical system activities.

Table 12: Decompiled code related to overlay attack

The window that the app may create will pass an event to an activity below e.g. touching by using the flag FLAG_NOT_TOUCHABLE. This mean the overlaying window cannot be touch directly (cannot see and cannot touch).

Android Accessibility Service

The Android’s Accessibility Services (AAS) is a feature designed for a user with some disabilities. It requires the user to enable it in device settings.

Table 13: Descriptive pop-up window while allowing AAS in Settings

Once the AAS is enabled for an app, the app will be able to help people with disabilities by doing something like:

  • read text and speak it,
  • change display settings e.g. colors, text size
  • perform voice or gesture recognition base on device capabilities.

This malware also asked the user for enabling accessibility for the app.

Table 14: The app requests for AAS

Here is the accessibility declaration in AndroidManifest.xml:

Table 15: Accessibility service declared in AndroidManifest.xml

Here is the Accessibility Service configuration in a.xml:

Table 16: Accessibility service setting declared in /res/xml/a.xml

Review on the AAS setting in a.xml, canRetrieveWindowContent attribute allows the app to retrieve content from the active window the user is on. accessibilityEventTypes="typeAllMask" this allows the AAS to handle all types of accessibility events including TYPE_WINDOW_CONTENT_CHANGED. This leads to at least 2 possibilities of its usage:

  • Read text from screen. Because the content change can be observed so it’s easy for AAS to know the content on the screen and log them.
  • Key logging. If a user types on the screen and the screen contents are changed, so the AAS can observe it.

Only AAS, if it is enabled on a device for a malware, it is enough for an attacker to perform most dangerous activities like stealing information. The AAS not only observes the screen but can perform action instead of the user. The following figure shows how it use AAS to perform ACTION_CLICK (16) by calling AccessibilityNodeInfo.performAction(16) on the screen without user even touch it:

Table 17: Sample of AAS performing click

Now the AAS can observe the content on screen then performing click. This leads to the following ability:

  • Automatically request a new permission and grant it. The AAS gives the app an ability of being noticed if window content is changed then the app will be able to perform click on the “Allow” button of the request permission window dialogue.

For an analyst who wants to analyze more, begin from the com.gibb.WebService class.

Dynamic Analysis

Class loading

Because the app was packed by some packer, this means it is difficult to statically analyze the app because most of the classes are not loaded if the app is not run. We need to run the app and monitor class loading. The following Frida script can be used to examine which external classes are loaded:

Java.perform(function() {
    // awaitForClassLoaded("com.js.main", traceClass); // does not work

    // let dalvik = Java.use("dalvik.system.DexFile");
    // let dalvik2 = Java.use("dalvik.system.DexClassLoader");
    let dexclassLoader = Java.use("dalvik.system.DexClassLoader");
    dexclassLoader.$init.implementation = function(a, b, c, d) {
        console.log(colors.green + " [!] DexClassLoader loads class from: ", a, colors.default)
        this.$init(a, b, c, d)
        try {
            // traceClass(this)
            if (a.includes("main")) {
                // console.log("[!] main.dex is loaded");
                // traceClass(this, "com.js.main");
            }
        } catch (e) {
            console.log(colors.red + e, colors.default)
        }
    }
});

The following figure shows the DexClassLoader loads classes from external files dropped by the app during run time:

Table 18: Loading class from external files

By using dynamic analysis, we can skip everything (unzip and decrypt files) done for retrieving main.dex (maindex.dex) and ui.dex because the app did it at run-time activity and place the decrypted files in “/files” directory. This means we can extract the decrypted file from the app folder in the “files” directory.

Table 19: The decrypted dex files dropped

After analyzing the app, it’s clear that the maindex.dex contains the main logics of the app. It may use other components like easyagent, ui.dex, and defaultplugin.apk for specific purpose but the heart of the app is in maindex.dex (at least we believe so, and it still the brain of the app apart from its heart).

Traffic Monitoring

The application communicates to its server using HTTPS and websocket. We manage to intercept the traffic using Burp suite with VPN server. By setting the VPN server and let the mobile connect to the VPN, we can arrange all traffics sent from the mobile to anywhere we want to. In this case, we redirect the traffic to Burp Suite. Anyway, the traffic is encrypted both in HTTPS and websocket. The following figure shows that app traffic via websocket:

Table 21: Sample of intercepted traffic

The traffic is ,of course, encrypted. To decrypt the WS traffic, the algorithm and the key must be known. We use the following Frida script to extract key and algorithm:

function get_IV() {
    //hooking IvParameterSpec's constructor to get the IV 
    var iv_parameter_spec = Java.use("javax.crypto.spec.IvParameterSpec");
    iv_parameter_spec.$init.overload("[B").implementation = function(x) {
        console.log(colors.yellow,'\t[!] IV found: ' + bytesToHex(new Uint8Array(x)),colors.default);
        return this.$init(x);
    }
}

function get_algorithm() {
    var Cipher = Java.use("javax.crypto.Cipher");
    var algo = null;
    Cipher.getInstance.overload("java.lang.String").implementation = function(x) {
        console.log("[!] Hooking javax.crypto.Cipher.getInstance");
        console.log("\t[!] Encryption algorithm: " + x);
        algo = x;
        return Cipher.getInstance.overload("java.lang.String").call(this, x);
    }
    return algo;
}

function get_crypto_info() {
    var secretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
    secretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(a, b) {
        var result = this.$init(a, b);
        console.log("======================================");
        console.log("[!] algoritm:" + b + "| HexKey:" + util_bytesToHex(new Uint8Array(a)));
        return result;
    }

    var encKey = "N/A";
    var secret_key_spec = Java.use("javax.crypto.spec.SecretKeySpec");
    var iv_parameter_spec_gcm = Java.use("javax.crypto.spec.GCMParameterSpec");
    var iv_parameter_spec = Java.use('javax.crypto.spec.IvParameterSpec');
    var cipher = Java.use("javax.crypto.Cipher");
    cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").implementation = function(x, y, z) {
        console.log("[!] Hooking AlgorithmParameterSpec");

        // 1 means Cipher.MODE_ENCRYPT 
        if (x == 1) { // 1 means Cipher.MODE_ENCRYPT 
            console.log("\t[!] Mode: MODE_ENCRYPT");
        } else {
            // In this android app it is either 1 (Cipher.MODE_ENCRYPT) or 2 (Cipher.MODE_DECRYPT)
            console.log("\t[!] Mode: MODE_DECRYPT");
        }
        encKey = util_bytesToHex(new Uint8Array(y.getEncoded()));
        console.log(colors.green,"\t[!] Key: " + encKey, colors.default);
        try {
            console.log(colors.yellow,"\t[!] IV: " + util_bytesToHex(new Uint8Array(Java.cast(z, iv_parameter_spec_gcm).getIV())),colors.default);
            console.log("\t[!] Tag length: " + Java.cast(z, iv_parameter_spec_gcm).getTLen());
        } catch (error) {

        }

        //init must be called this way to work properly
        return cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").call(this, x, y, z);

    }


    cipher.doFinal.overload("[B").implementation = function(x) {
        console.log("\t[!] Calling doFinal(Byte) ");
        console.log("\t\t[!] Before doFinal: " + util_bytesToHex(new Uint8Array(x)));
        var ret = cipher.doFinal.overload("[B").call(this, x);
        console.log("\t\t[!] After doFinal: " + util_bytesToHex(new Uint8Array(ret)));
        return ret;
    }

    cipher.doFinal.overload("[B", "int", "int").implementation = function(x, y, z) {
        console.log("\t[!] Calling doFinal(Byte, int, int) ");
        console.log("\t[!] Before doFinal: " + util_bytesToHex(new Uint8Array(x)));
        var ret = cipher.doFinal.overload("[B", "int", "int").call(this, x, y, z);
        console.log("\t[!] After doFinal: " + util_bytesToHex(new Uint8Array(ret)));
        return ret;
    }

    cipher.update.overload("[B").implementation = function(x) {
        console.log("\t[!] Calling update(Byte) ");
        console.log("\t\t[!] Before update: " + util_bytesToHex(new Uint8Array(x)));
        var ret = cipher.update.overload("[B").call(this, x);
        console.log("\t\t[!] After update: " + util_bytesToHex(new Uint8Array(ret)));
        return ret;
    }

    cipher.update.overload("[B", "int", "int").implementation = function(x, y, z) {
        console.log("\t[!] Calling update(Byte, int, int) ");
        console.log("\t[!] Before update: " + util_bytesToHex(new Uint8Array(x)));
        var ret = cipher.update.overload("[B", "int", "int").call(this, x, y, z);
        console.log("\t[!] After update: " + util_bytesToHex(new Uint8Array(ret)));
        return ret;
    }

    cipher.updateAAD.overload("[B").implementation = function(x) {
        console.log("\t[!] Calling updateAAD(Byte) ");
        console.log("\t[!] AAD: " + util_bytesToHex(new Uint8Array(x)));
        var ret = cipher.updateAAD.overload("[B").call(this, x);
        return ret;
    }
}

Once the app encrypts data before sending it to the server, the javax.crypto.Cipher.init() will be called with the java.securityKey as one of its arguments.

Table 22: The key used for encrypting WS request content

The following is an example of WS request message:

Table 23: Decrypting the WS request using the found key

To decrypt other traffic, we don’t prove that how does the app do but we presume that it could be possible to extract the key using Frida and use it for decrypting other traffic e.g. HTTP request/response to/from *.xdrig.com which is using RC4/ECB/NoPadding with hardcoded key to encrypt its content.

IoC

APK (MD5)39425fd18017c751d546b3f839495e8a
maindex.dex (MD5)00563ef29eb24da1e9b6c5717ca81da3
tg.iapk (MD5)f91773645d3fed91e99d7d19c2c01c70
ui.dex (MD5)7fc744141c4bacdfc38529b203aee8d5
defaultplugin.apk (MD5)51a211128d77f9d567b2900f2f7be63e
package installedcom.gzrtnq.Bumble

For a normal user who wants to know if the app is installed on your phone, go to Settings > Apps > Manage apps and search for Bumble. Check the app package name and uninstall it if it matches with the information above.

If the user cannot uninstall it by using UI, the ADB tool may be needed and should be executed by someone who knows what he is doing. To stop the app we need ADB by running the following command:

$ adb shell am force-stop com.gzrtnq.Bumble

Once the app stop, the ACC will be revoked from the app. But recommend removing it. To uninstall the app we need ADB by running the following command:

$ adb uninstall com.gzrtnq.Bumble

Summary

Android Accessibility Service is the most dangerous powerful permission which needs to be enabled by navigating through Android Settings screen can be used for many purposes. It was known for many years that there are malware using this capability to attack financial apps. We’re looking for the proper solution for developer to help them mitigate the attack from Overlay Attack and Accessibility capability. CoinBase app used to detect the accessibility and inform the users if the AAS is enable and interact with CoinBase app. The user needs to shake their phone in order to continue using the app with AAS enabled.

Anyway, we would like to warn any user who’s using Android phone to not install an app outside the Play Store. Google is currently trying to protect their users by preventing an app that misuses the AAS from being listed on Play Store so why would you like to side load it…