Decrypting Mega Preferences (Part 2)

The first post seemed to gain a lot of attention from people, with someone from a Police force contacting me to help with one of their cases, so I’ve spent a little time making the script into more of a finished product than a POC. The new script can be found here and is now completely python, though you will need to install the pycryptodomex library. Dont worry, I’ll cover everything needed in this post, including how to run it on air gapped machines, as a lot of Digital Investigations takes place on these types of machines

In the previous post, I took the Java code and made a small .jar ‘compiled’ executable that was called by a python script to decode data. This was the simplest way to do it, but had several limitations and I found examples where fields were not encrypted or had NULL values and this caused issues.

The TLDR; is that the new version of the script can be downloaded from here. Usage of the script is:

python megapreferencedecryptor1.0 -i {inputdatabase} -o {outputname}

Improving the Script

So, in order to change the Java code to Python, I needed to identify what algorithm was being used. This starts with an examination of Java “aes_decrypt” function code:

public static byte[] aes_decrypt(byte[] bArr, byte[] bArr2) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
        Cipher instance = Cipher.getInstance("AES");
        instance.init(2, secretKeySpec);
        return instance.doFinal(bArr2);
    }

Documentation of the Cipher.Init function, instance.init in the code, can be located here. One of these seems to match our implementation that takes two values, opmode and Key

Values for opmode as shown here, but the ones we are interested in are:

  • DECRYPT_MODE == 2
  • ENCRYPT_MODE == 1

I’m not sure whether the values have been hardcoded into the Mega App or if this is just how the decompilation process has interpreted it, but either way it doesn’t take us any further.

Looking at the creation of the secretKeySpec object we can see the string “AES” as a parameter as well as a byte array, which was the key we had previously located. However there are a number of different AES methods that can be used by Java and in order to decode the data in Python, we need to know exactly which one. As nothing else seems to be specified, we can assume there is a default method.

This StackOverflow post states that default method used in Java is AES/ECB/PKCS5Padding. A description of the ECB method of AES can be seen here

PKCS5Padding is described here: here. But basically it pads to an 8 byte boundary, so the size of the data is exactly divisible by 8, using a byte padding that has a value equal to the number of bytes padding.

If we had a block of 5 bytes, then 3 bytes of pading would be required, so the padding would by 0x030303. Similarly if only a single byte of padding was required the value 0x01 would be used.

The commonly found function for unpadding (I only need to unpad as I am decrypting not encrypting) looks like this

unpad = lambda s : s[0:-ord(s[-1])]

Note in Python3, which my script is written in as all new code should be, the “ord” function needs to be removed for it to work. So now we have a function to unpack the data, but we still need to decrypt it.

The code for decrypting the data using AES is suprisingly simple using the pycrypto function:

import base64
from Crypto.Cipher import AES

BS = 16

def unpad(ct):
    return ct[:-ct[-1]]

secretKey = "android_idfkvn8 w4y*(NC$G*(G($*GR*(#)*huio4h389$G"[0:32]
cipher = "IdAI0AprbXDgp5RH4ByJz/hxDJjUGPi8fLOI2lVMJgs="

private_key = secretKey
enc = base64.b64decode(cipher)
cipher = AES.new(bytes(private_key, 'utf-8'), AES.MODE_ECB)

print(unpad(cipher.decrypt(enc)).decode('utf-8'))

This code prints out the correct data for that string, so now we need to make it decode all the fields in our database. In the first POC script, the script created a new table within the original SQLite database with “dec” appended and this is less than ideal.

So in the more polished script, I take 2 arguments; the input database and an output name. This the gives a completely decrypted database without altering the original. So the actual decryption functions are shown below.

def unpad(ct):
    return ct[:-ct[-1]]

def decrypt(cipher):
    secretKey = "android_idfkvn8 w4y*(NC$G*(G($*GR*(#)*huio4h389$G"[0:32]
    private_key = secretKey
    enc = base64.b64decode(cipher)
    cipher = AES.new(bytes(private_key, 'utf-8'), AES.MODE_ECB)
    return(unpad(cipher.decrypt(enc)).decode('utf-8'))

So that takes care of the decryption section, but I need to read the database and extract all the tables, decrypt them and write them to a new database. To do this, I created a number of functions. The function below creates an

def generate_output_database(inputDB, outputDB):
    input_cursor = inputDB.cursor()
    output_cursor = outputDB.cursor()
    for row in input_cursor.execute("Select sql from SQLITE_MASTER where type = \"table\";"):
        output_cursor.execute(row[0])
    outputDB.commit()

This function is reading all the records from the SQLITE_MASTER table, which stores all the details of tables in the database, and executing the SQL command to create identical tables in the new database. Now all that is required is some code to decrypt the fields and write them to the new database.

def decrypt_tuple(field_data):
    if isinstance(field_data, str):
        if field_data == "":
            return "";
        else:
            return decrypt(field_data.replace("\n",""))
    elif isinstance(field_data, type(None)):
        return "NULL"
    elif isinstance(field_data, int):
        return str(field_data)
    else:
        return "Unknown"

def decrypt_table(inputDB, outputDB, table_name):
    input_cursor = inputDB.cursor()
    output_cursor = outputDB.cursor()
    for row in input_cursor.execute("Select * from %s;" % table_name):
        if table_name in ignore_tables:
            break
        record_data = []
        for column in row:
            record_data.append(decrypt_tuple(column))
        write_record(outputDB, table_name, record_data)
        

def decrypt_database(inputDB, outputDB):
    input_cursor = inputDB.cursor()
    output_cursor = outputDB.cursor()
    for row in input_cursor.execute("Select tbl_name from SQLITE_MASTER where type = \"table\";"):
        print("Extracting records from " + row[0])
        decrypt_table(inputDB, outputDB,row[0])

These can be summarised as the decrypt_database function gets the name of every table in the database and runs the decrypt table for each of them.

The decrypt_table function gets every row in the table and then passes every field to the decrypt_tuple function and writes the results into the output table.

The decrypt_tuple function checks the data type that it has received and deals with it appropriately. Where it is an encrypted Base64 encoded string, it decrypts it. If it is an empty value (NULL) then it returns the string “NULL” and if its a integer it returns a string of that integer.

That’s the basic overview of the script, the rest of the code is error checking and parsing arguments for the script. These can be seen in the full version.

Lastly, the pycryptodomex library is required to run the script. This is very simple to do on a machine with an Internet connection using the pip installer

$ pip install pycryptodomex
Collecting pycryptodomex
  Downloading pycryptodomex-3.14.1-cp35-abi3-manylinux2010_x86_64.whl (2.0 MB)
     |████████████████████████████████| 2.0 MB 20.6 MB/s 
Installing collected packages: pycryptodomex
Successfully installed pycryptodomex-3.14.1

This is tricky to do without an Internet connection and the easiest way is to install the same version of python on an Internet connected machine as on your offline machine and then use the following command to download the Wheel (.whl) file for the library

pip download --only-binary=:all: pycryptodomex

Once the Wheel file has been downloaded, it can be transferred to the offline machine and installed using the following command

pip install pycryptodomex-3.14.1-cp35-abi3-win_amd64.whl

That’s it, hopefully this has been useful and the script works for you.

2 thoughts on “Decrypting Mega Preferences (Part 2)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: