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.
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.
Application Structure
By statically reviewing the application, here is its 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 Requested | Description |
android.permission.CALL_PHONE | Allows the application to call phone without user interaction. |
android.permission.PROCESS_OUTGOING_CALLS | Allows the application to alter outgoing call e.g. change out going call number or even drop or redirect the call. |
android.permission.GET_TASKS | Allows the application to retrieve currently running tasks. |
android.permission.MOUNT_UNMOUNT_FILESYSTEMS | Allows the application to mount and unmount file systems. |
android.permission.READ_SMS | Allows the application to read SMS e.g. OTP |
android.permission.SEND_SMS | Allows the application to send an SMS message. |
android.permission.RECORD_AUDIO | Allows the application to record audio. |
android.permission.REQUEST_INSTALL_PACKAGES | Allows the application to request the user for installing another package. |
android.permission.WRITE_APN_SETTINGS | Allows the application to modify cellular APN. |
android.permission.WRITE_SETTINGS | Allows the application to modify the device settings. |
android.permission.INJECT_EVENTS | Allows the application to send input events (screen tap etc.) to another application. |
android.permission.SYSTEM_ALERT_WINDOW | Allows the application to create a window on top of other activities. (lead to overlay attack) |
android.permission.BIND_ACCESSIBILITY_SERVICE | Allows the application to access content of the running application (including other applications) |
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 name | tg.iapk |
File signature | 634e70a18127375f47c7b40564142ca5597640056a1745b701f7cec3c7701ff1 |
File Size | 2 MB |
File Type | ZIP & Password Protected & Encrypted |
Decrypting tg.iapk
After trying to extract the file tg.iapk, it was found that the file was achieved with password.
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):
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] |
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:
The key used for encrypting the file is “m” or 0x6d. We can use it to decrypt the file content:
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:
tg.iapk Structure
The following figure shows the 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.
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.
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.
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.
Here is the accessibility declaration in AndroidManifest.xml:
Here is the Accessibility Service configuration in 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:
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:
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.
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:
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.
The following is an example of WS request message:
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 installed | com.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…