Unpacking a JsonPacker-packed sample
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”).
In there, head to the object fields which get their initial value in the constructor. Spot the json file hq.json.
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.
Go to that class, and search for DexClassLoader, you find method rigidmiddle.
Now, work your way up to who calls this method, using cross-references:
tailcreekguessextraaerobicneutralattachBaseContext: the call toaeronicneutralis at the end of the image below.
Spot where payload decryption occurs
loadoverconstructs the absolute path of the filename.ceilingnicedecrypts the fileaerobicneutralloads the decrypted file.
In ceilingnice, let’s follow the calls to the decryption algorithm:
allratherorcharddecide
Method orcharddecide loads the asset:
Then it reads the asset, decrypts it (this happens inside futureinherit), unzips the result and writes it to the output stream.
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.
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.
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:
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:
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!
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.
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
