Unpacking a JsonPacker-packed sample

@cryptax
5 min readJun 27, 2022

With students of mine, we built a static unpacker (short of public name, I named it “JsonPacker” in APKiD). Unfortunately, the static unpacker doesn’t work for the sample and the students are in for a quick patch before their defense in a few days. Lol. I’m sure they hate me 😁 The sha256 of the sample is 2877b27f1b6c7db466351618dda4f05d6a15e9a26028f3fc064fa144ec3a1850, and it dates back to February 2022.

Quickly spot the encrypted json filename in the code

There are many classes, and obfuscated names, so at first it could be a bit disorientating to find the right spot. But I’ve unpacked such samples dozens of time: just search for the class with attachBaseContext (which is a method found in classes which derive from Application and which is called at “the very beginning”).

Head to class CToKhLqQwJbTrQrKg :)

In there, head to the object fields which get their initial value in the constructor. Spot the json file hq.json.

The encrypted payload is inside hq.json. I like to rename the field to something more meaningful :)

Spot the place where the file is dynamically loaded

For such samples, just look where DexClassLoader is used. I like to use the detailed report of DroidLysis for that.

DexClassLoader is used in a single place: ABeJgOnNtJpIcNgRxUkDwXcIwNyTzCyFxXhUsZsWxQuShDpLkUiRyWn

Go to that class, and search for DexClassLoader, you find method rigidmiddle.

This method loads dynamically the decrypted payload stored on the filesystem in “filename”

Now, work your way up to who calls this method, using cross-references:

  • tailcreek
  • guessextra
  • aerobicneutral
  • attachBaseContext: the call to aeronicneutral is at the end of the image below.
Parts of code of attachBaseContext. There is lots of junk code. The payload filename is used 3 times in this screenshots: loadover, ceilingnice, aerobicneutral

Spot where payload decryption occurs

  • loadover constructs the absolute path of the filename.
  • ceilingnice decrypts the file
  • aerobicneutral loads the decrypted file.

In ceilingnice, let’s follow the calls to the decryption algorithm:

  • allrather
  • orcharddecide

Method orcharddecide loads the asset:

First, the code retrieves an AssetManager. Then it opens the encrypted payload asset. The input stream is the encrypted payload, the output stream will be stored in the location of absolutepath

Then it reads the asset, decrypts it (this happens inside futureinherit), unzips the result and writes it to the output stream.

This part of code (inside orcharddecide) decrypts the assets and unzips the result.

futureinherit calls ratherbanana. It takes an encrypted byte array as input and returns a decrypted byte array.

Understanding the preparation of the key

ratherbanana reads a fixed string (“Ianj” for this sample), and I assume it is the key. It converts the string to a byte array, then converts it to an integer array in nomineesign.

Still lots of junk code. Prepare the decryption key.

The code of nomineesign is not very long but requires close attention to remove junk code (but not too much: the loop initializing the convertedkey table is not junk!), and de-obfuscate code.

The line with StrictMath.hypot is obfuscated.

For example, the lign with hypot is interesting:

int cv = (int)StrictMath.hypot(this.timeone('d', 0x1E681L, convertedkey, index), 0.0);

timeone actually returns the index-th byte of convertedkey, i.e convertedkey[index].

hypot computes square root of ( convertedkey[index]² + 0² ). As 0² = 0, the variable cv will simply receive the value of convertedkey[index].

In the end, the algorithm boils down to this:

private void swap(int a, int b, int[] array) {
int tmp = array[a];
array[a]=array[b];
array[b]=tmp;
}

private int[] convert_key(byte[] key) {
int[] convertedkey = new int[0x100];
int i;
for(i = 0; i < 256; ++i) {
convertedkey[i] = i; // init
}
int j = 0;
int k = 0;
while(j < 0x100) {
int cv = convertedkey[j];
k = (k+cv+key[j%key.length]+0x100) % 0x100;
swap(j, k, convertedkey); // swap values
++j;
}
return convertedkey;
}

Decryption algorithm

The next step is to understand the decryption algorithm in itself. Actually, there is lots of junk code that can be removed. To start, I focus on where the encrypted input byte array is used.

decrypted[i] = this.motionavoid(Math.round(v0_6) ^ encrypted[i]);

The method motionavoid is there just for obfuscation: it merely returns its argument. Also, obviously, we only have integers, so Math.round is useless. So, we have decrypted[i] = v0_6 ^ encrypted[i];. A few lines above, we have v0_6: int v0_6 = ckey[(v15 + v0_5) % 0x100];. A few lines above, we have v15 and v0_5:

int v15 = this.timeone('b', 5222L, ckey, HMoEsEkXySsLhTyCkZlChSoBfFlPk.counter);  // ckey[counter]
this.GfnxRHLRQuDY_713808 = this.KfYicpzIQMgk_598597 * 0x12FC3 + this.RMSmhfBNuxnA_506561 - 50009; // junk
int v0_5 = this.timeone('z', 0x179161L, ckey, v14); // ckey[v14]

The method timeone only uses the last two arguments: a table and an index, and returns table[index] value. Quite strangely, v15 uses a static integer that I have renamed counter. I search where this counter is used:

The first use basically increments the counter, making sure it remains below 0x100. Then, counter is put in v2 and swapped with another value (energyalmost is a method that performs byte swap). Finally, v15 gets the value of the ckey[counter]

I work out that int v15 = ckey[counter];.

As for v0_5, we have v0_5 = ckey[v14] and v14 is yet another static counter: int v14 = HMoEsEkXySsLhTyCkZlChSoBfFlPk.other_counter;. Same, I search where this other counter is used, and it’s basically the same: an increment modulus 0x100, a swap and ckey[other_counter]. That’s it! We have all elements to decrypt! The algorithm boils down to this:

Simplified decryption method. For this sample, the initial key is “Ianj”. The encrypted byte array is the contents of hq.json. I added a length argument because actually in my code the hq.json is read into a bigger array, and we only need to decrypt up to the length of hq.json file.

Note: the code above uses static variables counter and other_counter, but actually it works fine with local variables, and probably would be easier to read with local ones.

Decrypt the payload

To the key + decryption algorithm, we just need to add something to read hq.json and write to another file. Then, we can unpack!

Static unpacker works fine :) Hurray!

The decrypted file is a Zip file (this was expected: remember that orcharddecide unzips the result): inside, there is a classes.dex (sha256: dae52bbee7f709fae9d91e06229c35b46d4559677f26152d4327fc1601d181be). It is the payload of the Xenomorph malware.

Which class/method does the malware load dynamically?

Before we decompile this payload, we need to know which method is called. The manifest shows the main activity is com.sniff.sibling.MainActivity. This class is not present in the wrapping apk… so it must be in the payload! This will be automatically called by Android as soon as it’s time to launch the main activity.

The main activity is indeed found in the payload.

We’ve had enough for a single blog post, but the payload, similarly to many Android botnets, uses the Accessibility Services API to overlay windows of given applications.

— Cryptax

--

--

@cryptax

Mobile and IoT malware researcher. The postings on this account are solely my own opinion and do not represent my employer.