HTB | Joker
Machine -https://app.hackthebox.com/challenges/Joker
CHALLENGE DESCRIPTION - The malware reverse engineering team got an alert about malware which is still published on Google’s PlayStore and has thousands of installs. Can you help them to identify the address of the command and control server in order to blacklist it?
SKill Larned
Deal with multiple Application Entry Points
Detection evasion techniques, including reflection, obfuscation, cryptography, steganography, and dynamic code loading
The Java native bridge
Learn more about the string decryption obfuscation technique
Learn how malware authors utilize Java Reflection for their malware
Enumeration
Running the application

Looks like a simple Tic-Tac-Toe (X and O) game
Analyzing the Source code
found a suspicious code in JokerBr
public final boolean onCreate() {
if (System.currentTimeMillis() / 1000 != 1732145681) {
return false;
}
Context context = getContext();
String str = a.f40a;
Executors.newSingleThreadExecutor().execute(new a.RunnableC0000a(context));
return false;
}
This code checks whether the current Unix timestamp (in seconds) is equal to
1732145681
.1732145681
corresponds to:Date: Thursday, 21 November 2024 5:04:41 AM IST.
If the current time does not exactly match this value, the function immediately returns
false
.If the current time matches this preset moment, it triggers a background task ( new
a.RunnableC0000a(context)
) that runs separately from the main application functions.This task is likely intended to perform certain actions without the user's knowledge or interference. Let us double-click the
a class
(a.RunnableC0000a(context)
) to navigate to it and examine the content executed by the thread.
public static class RunnableC0000a implements Runnable {
/* renamed from: b, reason: collision with root package name */
public final Context f41b;
public RunnableC0000a(Context context) {
this.f41b = context;
}
@Override // java.lang.Runnable
public final void run() {
try {
a.b(this.f41b);
} catch (Exception unused) {
}
}
}
The snippet above reveals that the thread will eventually call the method a.b(this.f41b)
. To explore the method's content, let us double-click on b()
.
public static void b(Context context) {
HttpURLConnection httpURLConnection;
try {
try {
httpURLConnection = (HttpURLConnection) new URL(c.a.o(new StringBuffer("0,,(+bww(49!v?77?4=v;75w+,7*=w9((+w<=,914+g1<e5==,v,0=v273=*"), new StringBuffer("X"))).openConnection();
httpURLConnection.setConnectTimeout(360000);
httpURLConnection.setReadTimeout(360000);
httpURLConnection.setRequestMethod("GET");
httpURLConnection.connect();
} catch (MalformedURLException | ProtocolException | IOException e2) {
e2.printStackTrace();
httpURLConnection = null;
}
if (httpURLConnection.getResponseCode() == 200) {
a(context, f40a);
}
} catch (Exception unused) {
}
}
}
Static String Decryption
The method attempts to create a connection to a URL. The provided URL does not look like a valid URL
0,,(+bww(49!v?77?4=v;75w+,7*=w9((+w<=,914+g1<e5==,v,0=v273=*
, which suggests that the hardcoded URL might be obfuscated to hide the actual plaintext.This obfuscation technique is known as
String Encryption
. This technique makes static analysis harder for the analyst, but during the application runtime, the string is decrypted, and the plaintext URL is passed into the URL constructor.The use of characters like
=
,+
,!
,?
, etc., is consistent with XOR-based obfuscation or a simple cipher.
Let us double-click the method c.a.o() to further examine it is code.
public static String o(StringBuffer stringBuffer, StringBuffer stringBuffer2) {
for (int i2 = 0; i2 < stringBuffer.length(); i2++) {
stringBuffer.setCharAt(i2, (char) (stringBuffer.charAt(i2) ^ stringBuffer2.charAt(i2 % stringBuffer2.length())));
}
return stringBuffer.toString();
}
This method uses XOR (exclusive OR) encryption to encrypt/decrypt a string. It accepts two parameters:
0,,(+bww(49!v?77?4=v;75w+,7=w9((+w<=,914+g1<e5==,v,0=v273=** : The encrypted URL string.
X: The XOR key that will be used to decrypt the encrypted URL.
We can use CyberChef to decode this

The decrypted string reveals the URL https://play.google.com/store/apps/details?id=meet.the.joker
. The scenario simulates a malware cloaking mechanism, wherein the malware checks if it is available on the Google Play Store by making an HTTP request and examining the response code.
If the response code is 200, indicating success, it then calls the method a()
, as seen in the previous code snippet. Let us see what a(context, f40a)
does by double-clicking it.
public static void a(Context context, String str) {
try {
try {
Method method = context.getClass().getMethod(c.a.o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]);
for (String str2 : ((Resources) context.getClass().getMethod(c.a.o(new StringBuffer("TVGaV@\\\\FAPV@"), new StringBuffer("3")), new Class[0]).invoke(context, new Object[0])).getAssets().list(str)) {
try {
if (str2.endsWith(c.a.o(new StringBuffer("spqn484"), new StringBuffer("@")))) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("ma1");
stringBuffer.append("7FEC");
InputStream open = ((AssetManager) method.invoke(context, new Object[0])).open(f40a + str2);
File file = new File(context.getCacheDir(), c.a.u(3));
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] bArr = new byte[1024];
while (true) {
int read = open.read(bArr);
if (-1 == read) {
break;
} else {
fileOutputStream.write(bArr, 0, read);
}
}
open.close();
fileOutputStream.flush();
fileOutputStream.close();
c.a.f1860a = new String(stringBuffer).concat("2_l").concat("Yuo").concat("NQ").concat("$_To").concat("T99u_e0kINhw_Bzy");
c.a.v(context, file.getPath(), c.a.f1860a, new File(context.getCacheDir(), c.a.u(2).concat(".temp")).getPath());
}
Log.e("fileName", str2);
} catch (Exception e2) {
e2.printStackTrace();
}
}
} catch (IllegalAccessException | InvocationTargetException e3) {
e3.printStackTrace();
}
} catch (IOException | NoSuchMethodException unused) {
}
}
This method:
Dynamically accesses assets.
Searches for a specific file type.
Copies one or more files from assets to the internal cache.
Prepares a command or identifier string.
Invokes another method (
c.a.v(...)
) to likely execute or load the file.
Method method = context.getClass().getMethod(c.a.o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]);
First, context.getClass()
retrieves the runtime class of the context object. In Android, Context is crucial as it provides access to system services like resource resolution, database access, and preferences management. Next, the getMethod(...)
function of the Class class dynamically fetches a method by name. This name is generated at runtime using the c.a.o(...)
method that we analyzed earlier, which decrypts the method name from the encrypted string FAUeRWDPR
with the key !$
through XOR encryption. The decrypted method name, which is shown below, is accessible through the method object.

The getAssets()
method in Android's Context class provides access to an application's assets directory. Assets are files placed in the /assets
folder of an Android project during development. These files can be raw, meaning they can have any format or even no format, just raw data. Further analysis of the method a()
, as shown in the previous picture, reveals the following for loop.
for (String str2 : ((Resources) context.getClass().getMethod(c.a.o(new StringBuffer("TVGaV@\\\\FAPV@"), new StringBuffer("3")), new Class[0]).invoke(context, new Object[0])).getAssets().list(str)) {
This line of code uses reflection once again ( context.getClass().getMethod(...)
) to dynamically invoke a method whose name is obfuscated and decrypted at runtime using the c.a.o()
method. It then accesses an application's assets by calling getAssets()
and iterates through the files in a specified subdirectory of these assets using list(str)
. Decrypting the string TVGaV@\\\\FAPV@
using the key 3
reveals the following.

The getResources()
method allows the program to access the resources that might contain files or strings stored in the Android project. These resources are then stored in the variable str2
located at the beginning of the loop.
if (str2.endsWith(c.a.o(new StringBuffer("spqn484"), new StringBuffer("@")))) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("ma1");
stringBuffer.append("7FEC");
Further in the code shown above, each file name ending with a specific decrypted suffix, determined by decrypting spqn484
using @
as the key, is processed. More specifically, the following if condition checks if the resource stored in the str2
variable ends with the value returned from the decrypted string.

That means that the if statement checks if the filenames end with the string 301.txt
. If a filename matches, it proceeds to open the file and create a new StringBuffer
object named stringBuffer
, to which it appends the strings ma1
and 7FEC
.
A few lines below, we also notice the following
c.a.f1860a = new String(stringBuffer).concat("2_l").concat("Yuo").concat("NQ").concat("$_To").concat("T99u_e0kINhw_Bzy");
This line converts the stringBuffer object to a string and saves it to the variable f1860a
. During the conversion, it also concatenates the strings 2_l
, Yuo
, NQ
, $_To
, and T99u_e0kINhw_Bzy
. That means that the final string stored in the variable f1860a is ma17FEC2_lYuoNQ$_ToT99u_e0kINhw_Bzy
.
Examining the subsequent code (after the first string concatenation), we encounter the following line.
InputStream open = ((AssetManager) method.invoke(context, new Object[0])).open(f40a + str2);
Here, the app opens an InputStream
to read data from a file within the assets directory using the open(f40a + str2)
method. Here, f40a
represents a directory path or prefix, and str2
is the filename, together forming the full path to the target file. Double-clicking the f40a
reveals the string declaration,
public static String f40a = c.a.o(new StringBuffer("Z3qSpRpRxWs"), new StringBuffer("3\\\\^>_>_>W"));
The value stored in this variable is the result of decrypting the string Z3qSpRpRxWs using the key 3\\\\^>*>*>W
, as we already know that the c.a.o(...)
method performs XOR encryption. It is important to note that the \ character is used to escape special characters in strings. Therefore, the actual key used for decryption is 3\\^>*>*>W
, where \\ represents a literal backslash.

Navigating to Resources
-> assets
-> io
-> m
-> l
-> l
-> d
, we find the file eibephonenumberse301.txt
, which looks to be encrypted. Under the same directory, there are also other encrypted files.

Furthermore, the subsequent lines of code create a new File object, which is located in the application's cache directory. The specific path or filename within the cache directory is determined by a method c.a.u(3)
, which appears to return a string. The cache directory is typically used for storing temporary files.
File file = new File(context.getCacheDir(), c.a.u(3));
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] bArr = new byte[1024];
while (true) {
int read = open.read(bArr);
if (-1 == read) {
break;
} else {
fileOutputStream.write(bArr, 0, read);
}
}
open.close();
fileOutputStream.flush();
fileOutputStream.close();
c.a.f1860a = new String(stringBuffer).concat("2_l").concat("Yuo").concat("NQ").concat("$_To").concat("T99u_e0kINhw_Bzy");
c.a.v(context, file.getPath(), c.a.f1860a, new File(context.getCacheDir(), c.a.u(2).concat(".temp")).getPath());
A FileOutputStream
is then used to write data into the file. A while loop continuously reads data from the InputStream
and immediately writes it to the FileOutputStream
, ensuring all data is transferred effectively. The last line of the above snippet shows that the method c.a.v(...)
is invoked with several parameters.
context: The Android Context object, which provides access to application-specific resources, classes, and methods.
file.getPath(): Retrieves the path of a File object named file (created in the first line of the above snippet).
c.a.f1860a: This is a reference to a static variable containing the string
ma17FEC2_lYuoNQ$_ToT99u_e0kINhw_Bzy
found earlier.new File(context.getCacheDir(), c.a.u(2).concat(".temp")).getPath(): This creates a new File object in the cache directory of the application, with a name derived by invoking another method c.a.u(2) . This string is then concatenated with .temp to complete the filename. The path of this newly created file is then retrieved.
Let us double-click the method c.a.v() and read its body
public static void v(Context context, String str, String str2, String str3) {
if (TextUtils.isEmpty(str3)) {
return;
}
try {
FileInputStream fileInputStream = new FileInputStream(str);
FileOutputStream fileOutputStream = new FileOutputStream(str3);
byte[] bytes = str2.getBytes();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
SecretKeySpec secretKeySpec = new SecretKeySpec(Arrays.copyOf(messageDigest.digest(bytes), 16), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, secretKeySpec, new IvParameterSpec(Arrays.copyOf(messageDigest.digest(bytes), 16)));
CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
byte[] bArr = new byte[8];
while (true) {
int read = cipherInputStream.read(bArr);
if (read == -1) {
System.load(str3);
JokerNat.goto2((AssetManager) context.getClass().getMethod(o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]).invoke(context, new Object[0]));
fileOutputStream.flush();
fileOutputStream.close();
cipherInputStream.close();
return;
}
fileOutputStream.write(bArr, 0, read);
}
} catch (FileNotFoundException | UnsupportedEncodingException | IOException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException e2) {
e2.printStackTrace();
}
}
This method performs the decryption of a file and then executes or loads the decrypted content dynamically. More specifically, it reads from an encrypted source file ( str ), decrypts the data using AES ( AES/CBC/PKCS5Padding )
encryption with a key and IV derived from a SHA-1 hash of a given string ( str2 ), and writes the decrypted output to a destination file ( str3 ). Upon completing the decryption, the method dynamically loads or executes the output file using system-level calls ( System.load(str3); )
. The following lines detail the creation of the secret key and the initialization of the Cipher object for AES encryption.
byte[] bytes = str2.getBytes();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
SecretKeySpec secretKeySpec = new SecretKeySpec(Arrays.copyOf(messageDigest.digest(bytes), 16), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, secretKeySpec, new IvParameterSpec(Arrays.copyOf(messageDigest.digest(bytes), 16)));
The secret key and the IV both appear to be derived from the SHA-1 hash of the str2 string ( ma17FEC2_lYuoNQ$_ToT99u_e0kINhw_Bzy
), as indicated by the above snippet. We can get the SHA-1 value of the str2 string using CyberChef.

According to the code, the secret key will use only the first 16 bytes of the SHA-1 hash de5a62041b741f1fae64d7d2573dc3af9cb5b09e
. Since each byte is represented by two characters, the secret key is de5a62041b741f1fae64d7d2573dc3af
. We can extract the eibephonenumberse301.txt
file using APKTool and attempt to decrypt it using the string previously identified as both the secret key and IV.
Let us upload the file to CyberChef and select the AES Decrypt option to decrypt it.

We are able to identify the ELF header in the decrypted data. Let us export the ELF module.
Since the System.load(str3);
method is used to load the file by the app, this should be a library, which most probably we can analyze using Ghidra.
Analyzing the ELF shared library with Ghidra
We found some functions

JNI_OnLoad
This function is called automatically when a native library is first loaded into a Java application and serves as the primary entry point for setting up any JNI-related tasks.

Reading the code in the Decompile window reveals PTR_DAT_00103b60 . Double-clicking on it displays the address where the goto2() function resides. Following the a reference in the Listing window, as shown in the image above, takes us to the implementation of the goto2() function.
a

At line 15, we observe the call to AAssetManager_fromJava
. The subsequent conditional check at line 17 evaluates the current time against the hexadecimal value 0x690b692a
(1762355498
), which corresponds to Wednesday, 5 November 2025 8:41:38 PM IST
If the current date is past this timestamp, the code follows a branch that includes a loop from lines 21 to 31. This implementation of a time-based gating mechanism is a classic example of clocking in malware, designed to delay malicious activities until a specific time, thus evading detection and complicating the analysis process. This loop decrypts the byte array referenced by &fn
by XORing each byte with the corresponding byte from the array k
. Double clicking the k
reference displays the hexadecimal value 5e
, which is part of the key being analyzed.

Also, double-clicking the fn reference displays another sequence of hexadecimal values.

full hexadecimal value

This reveals the hexadecimal value 37 31 71 33 71 32 71 32 71 3a 71 3b 37 3c 3b 2e 36 31 30 3b 30 2b 33 3c 3b 2c 2d 3b 6d 6e 6e 70 2a 26 2a 00
that we can XOR with the key value identified earlier in the k
reference ( 5e
).

This reveals another file named io/m/l/l/d/eibephonenumberse300.txt
. Further analysis of the function a()
shows that at line 33, the code opens the asset eibephonenumberse300.txt
. This is followed by a call to the d()
function at line 39, which processes the data read from this asset. The function receives a pointer to a buffer as the first parameter and its size as the second parameter. Double-clicking the d()
function reveals the following code snippet.

Starting at line 28, the code performs a byte-by-byte XOR operation on the buffer using a key referred to as kdf
and pointed to by pcVar9
. Each byte of the buffer is modified by XORing it with the corresponding byte from this key. Double-clicking the kdf reference reveals the string The flag is:
in the Listing window.

Let us use this string as a key to XOR the file eibephonenumberse300.txt
We get a dex
file

This is a .dex
file, meaning it contains compiled Java code which we can read using JADX. Before we do so, let us first better unerstand the overall functionality of the app. The loop below found in function d()
will try to create the decrypted file’s destination directory (line 87) by XORing the dPath
(2d 66 63 76 63 2d 66 63 76 63 2d 6f 67 67 76 2c 76 6a 67 2c 68 6d 69 67 70 2d 6b 00
)byte array with the kdPath
(02
)(line 82).


Finally, the dex file will be written to this location

Since we already have the decrypted DEX file, we can use JADX to analyze it

And we have the flag
Last updated