Joker – Hack The Box – @lautarovculic

Difficult: Hard

Category: Mobile

OS: Android

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 ?

Download and extract the .zip file with hackthebox as password.

Decopile the .apk file with apktool

				
					apktool d joker.apk
				
			

I’ll use an Android 12 (SDK 31) with genymotion

Install it

				
					adb install -r joker.apk
				
			

Let’s inspect the source code with jadx

After see many code lines, this method catch my attention

				
					    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;
    }
				
			

Because there are a logic error, the following code

				
					        Context context = getContext();
        String str = a.f40a;
        Executors.newSingleThreadExecutor().execute(new a.RunnableC0000a(context));
        return false;
				
			

Never will execute.

Searching for f40a string, we can found this line

In a2 → a class

				
					public static String f40a = c.a.o(new StringBuffer("Z3qSpRpRxWs"), new StringBuffer("3\\^>_>_>W"));
				
			

That is stored in c.a.o()

We cann see the XOR logic of the function, XOR arg 1 and arg 2, for get a “legible string”.

				
					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();
    }
				
			

We need patch the code, this

				
					    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;
    }
				
			

So we need patch it change the if-nez to if-eqz in the smali code

The smali code look has

				
					.method public final onCreate()Z
    .registers 6

    invoke-static {}, Ljava/lang/System;->currentTimeMillis()J

    move-result-wide v0

    const-wide/16 v2, 0x3e8

    div-long/2addr v0, v2

    const-wide/32 v2, 0x673e7211

    cmp-long v4, v0, v2

    if-nez v4, :cond_20

    invoke-virtual {p0}, Landroid/content/ContentProvider;->getContext()Landroid/content/Context;

    move-result-object v0

    sget-object v1, La2/a;->a:Ljava/lang/String;

    invoke-static {}, Ljava/util/concurrent/Executors;->newSingleThreadExecutor()Ljava/util/concurrent/ExecutorService;

    move-result-object v1

    new-instance v2, La2/a$a;

    invoke-direct {v2, v0}, La2/a$a;-><init>(Landroid/content/Context;)V

    invoke-interface {v1, v2}, Ljava/util/concurrent/Executor;->execute(Ljava/lang/Runnable;)V

    :cond_20
    const/4 v0, 0x0

    return v0
.end method
				
			

We need change if-nez v4, :cond_20 to if-eqz v4, :cond_20

In this file:

				
					/joker/smali/meet/the/joker/JokerBr.smali
				
			

Then now, rebuild the apk

I’ll skip the process, because there are so many easy and medium writeups on my web about this.

But if you are getting this errors

				
					adb install -r joker/dist/joker_aligned.apk
Performing Streamed Install
adb: failed to install joker/dist/joker_aligned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed collecting certificates for /data/app/vmdl1718695575.tmp/base.apk: Failed to collect certificates from /data/app/vmdl1718695575.tmp/base.apk: META-INF/LAUTARO.SF indicates /data/app/vmdl1718695575.tmp/base.apk is signed using APK Signature Scheme v2, but no such signature was found. Signature stripped?]
				
			
				
					adb -s 192.168.56.105:5555 install -r joker/dist/joker_aligned.apk
Performing Streamed Install
adb: failed to install joker/dist/joker_aligned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl2026314334.tmp/base.apk: META-INF/LAUTARO.SF indicates /data/app/vmdl2026314334.tmp/base.apk is signed using APK Signature Scheme v2, but no such signature was found. Signature stripped?]
				
			
				
					adb install -r joker/dist/joker_aligned.apk
Performing Streamed Install
adb: failed to install joker/dist/joker_aligned.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Scanning Failed.: No signature found in package of version 2 or newer for package meet.the.joker]
				
			

Don’t use the aligned apk with zipaling and just install the signed apk in an Android SDK 29.

				
					adb -s 192.168.56.105:5555 install -r joker/dist/joker_signed.apk
Performing Incremental Install
Serving...
Unknown command: install-incremental
Performing Streamed Install
Success
				
			

Then, now open jadx with the joker_signed.apk

And we can see that the code now is patched

				
					 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;
    }
				
			

For know what function is called, we can see

a2 → a and c → a

a2.a.b() is calling a → c.a.o()

Here is 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) {
        }
    }
				
			

The URL is a XOR that we can brute with CyberChef

URL

				
					https://play.google.com/store/apps/details?id=meet.the.joker
				
			

And looking the GET request

				
					 if (httpURLConnection.getResponseCode() == 200) {
                a(context, f40a);
 }
				
			

This code will never executed because if go to the URL, we receive a 404 not found.

Then, the a(); function will not executed.

So, we need again, patch the apk file.

The smali code that we need modify is

				
					if-ne v0, v1, :cond_45
				
			

We need change if-ne to if-eq in

/joker/smali/a2/a.smali

Rebuild the new apk

Then, installing the new apk and if we go to a2 → a → b function

We’ll look the ! =

Now we leave b(), the app will entry to a2.a.a()

				
					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) {
        }
    }
				
			

We can see that the method is reading for some files in assets.

And we have the for and if functions when the filename ends in 301.txt, then, call c.a.v()

				
					if (str2.endsWith(c.a.o(new StringBuffer("spqn484"), new StringBuffer("@")))) {
				
			

Here we can find the “txt” files.. (they are binaries).

Returning to c.a.v()

Here is the method

				
					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();
        }
    }
				
			

str2 is the key, and second arg

And, the first arg is str, that is 2 (DECRYPT_MODE) in AEScipher.init()

So now, we just know the str3 value.

Looking in the previous java code, we can see that some is created as temp file.

Go to

				
					/data/user/0/meet.the.joker/cache/
				
			

And check the file ll.temp

				
					vbox86p:/data/user/0/meet.the.joker/cache # file ll.temp
ll.temp: ELF shared object, 64-bit LSB arm64
				
			

Here’s a ELF format, probably this is a C native library that we need inspect in deep.

Then

				
					adb pull /data/user/0/meet.the.joker/cache/ll.temp /home/lautaro/Desktop/CTF/HTB/mobile/joker/joker2/ll.temp
				
			

We can see this XOR string in a() function

And here is the text

Looking the java source code, we have assetManager in JokerNat

				
					package meet.the.joker;

import android.content.res.AssetManager;

/* loaded from: classes.dex */
public class JokerNat {
    public static native void goto2(AssetManager assetManager);
}
				
			

And inspecting in the line 27 in the a() function of ll.temp, we can see that the eibephonenumerose300.txt content is transferred to d() method:

In d() method, we can see again in the line 27

Following the kdf value is “The flag is:” for XOR the .txt file.

Then, in line 75 we can see that there is the previous XOR stored in

				
					/data/data/meet.the.joker/i
				
			

This conditions isn’t true, then we can try upload the eibephonenumberse300.txt file in CyberChef and try “The flag is:” as the key.

And we can found the flag.

Note: M and T in the flag string is lowercase.

I hope you found it useful (:

Leave a Reply

Your email address will not be published. Required fields are marked *