InsomniDroid - 2012

16 August 2019

APK to download

First contact

After downloading the APK from the github repository we fire up android studio and the emulator. We install the APK by dragging it into the emulator interface and launch the app.

The application consists of a text field where we can enter a secret code and a button "Validate" to supposedly validate the code.

If we try a random serial the text "Sorry, try harder!" is displayed.

Static analysis with JADX

We start our reverse engineering with a static analysis of the application. We use JADX to do this.

We can see that there aren't a lot of classes :

  • Compute
  • InsomniActivity

In InsomniActivity we can see what happens when we click the button "Validate" :

this.validateBtn.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
        if (Compute.checkSecret(InsomniActivity.this.editTxt.getText().toString())) {
            InsomniActivity.this.txtView.setText("Congrats! -- FortiGuard Team");
            InsomniActivity.this.editTxt.setEnabled(false);
            return;
        }
        InsomniActivity.this.txtView.setText("Sorry, try harder!");
    }
});

The method checkSecret is called with the text we entered. If the method returns true we won. As we can see below, checkSecret computes the SHA-256 checksum of the text we entered and compares it to the digest secretHash which is defined in the same class.

public static boolean checkSecret(String input) {
    try {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.reset();
        if (Arrays.equals(secretHash, digest.digest(input.getBytes()))) {
            return true;
        }
    } catch (Exception e) {
        Log.w("InsomniDroid", "checkSecret: " + e.toString());
    }
    return false;
}

First cracking attempt

Seeing that we have a hash to crack I thought that it might be worth a try passing it to hashcat or john the ripper.

First I retrieved the hash. Here is the Kotlin code I used to do so:

fun main() {
    println(Compute.secretHash.joinToString("") { byte ->  String.format("%02X", byte) })
}

class Compute {
    companion object{
        val secretHash = byteArrayOf(
            97,
            82,
            88,
            126,
            -34,
            -118,
            38,
            -11,
            63,
            -45,
            -111,
            -80,
            85,
            -44,
            -34,
            80,
            30,
            -24,
            -78,
            73,
            java.lang.Byte.MAX_VALUE,
            -25,
            79,
            -113,
            -42,
            -97,
            44,
            114,
            -30,
            -13,
            -29,
            122
        )
    }
}

The hash is 6152587EDE8A26F53FD391B055D4DE501EE8B2497FE74F8FD69F2C72E2F3E37A. I passed it to hashcat : hashcat -a 3 -m 1400 hash.txt --increment but after an hour I let it go, it wouldn't find anything.

Looking somewhere else

The class Compute defines another function named compute(). Here is its code :

public static void compute(byte[] keyBytes2) {
    try {
        SecretKeySpec key = new SecretKeySpec(keyBytes2, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
        cipher.init(2, key, ivSpec);
        cipher.doFinal(c1_null);
        byte[] p2 = new byte[c2.length];
        for (int i = 0; i < c2.length; i += 16) {
            cipher.init(2, key, ivSpec);
            cipher.doFinal(c2, i, 16, p2, i);
        }
    } catch (Exception e) {
        Log.w("InsomniDroid", "compute: " + e.toString());
    }
}

The function starts by constructing a secret key from keyBytes2. Then the function defines an initialization vector from ivBytes. An instance of the AES-CTR cipher is then created and initialized from the secret key and the initialization vector.

The parameter 2 is the operation mode. In our case 2 means DECRYPT_MODE. The cipher is then used to decrypt c1_null and c2 by blocks of 16 bytes.

The awkward thing about compute() is that it does not print or return anything.

If we try to display keyBytes or c1_null we get garbage. If we try to display the result of c1_null decryption we also get garbage :

cipher.init(2, key, ivSpec)
val doFinal = cipher.doFinal(c1_null)
println(doFinal.toString(Charsets.UTF_8)) // Only garbage

Brushing up on my crypto

One thing that stands out from compute() is the fact that the initialization vector is reused everytime to do the decryption.

byte[] p2 = new byte[c2.length];
for (int i = 0; i < c2.length; i += 16) { // Decryption by blocks of 16
    cipher.init(2, key, ivSpec); // Same initialization vector is reused every time
    cipher.doFinal(c2, i, 16, p2, i);
}

Even if I am not a crypto expert I remember vividly that reusing an initialization vector is one of the worst thing you can do in cryptography, so there might be a way to exploit this.

From the Wikipedia page on AES-CTR here is the interesting part :

Once an attacker controls the IV–counter pair and plaintext, XOR of the ciphertext with the known plaintext would yield a value that, when XORed with the ciphertext of the other block sharing the same IV–counter pair, would decrypt that block.

Here is a beautifully drawn ASCII diagram of what should be happening.

                     IV                      New IV (O0)
                      +         +--------------+
                      |         |              |
               +------v-----+   |       +------v-----+
               |            |   |  key  |            |
    key +------>   CIPHER   |   |   +   |   CIPHER   |
               |            |   |   +--->            |
               +------------+   |       +------------+
                     |          |              |
                     +----------+              |
                     |                         |
                     v                         v
    plaintext  +-->(XOR)       plaintext +-->(XOR)
    (P0)             +         (P1)            +
                     |                         |
                     v                         v
                 ciphertext (C0)       ciphertext (C1)

The first time, we generate E(key, IV) = O0 and we have C0 = P0 ⨁ O0. The second time, we have E(key, O0) = 01 and C1 = P1 ⨁ 01. This continues until all of the plaintext has been encrypted.

In our case what really happens is this:

                 IV                       IV
                  +                        +
                  |                        |
           +------v-----+           +------v-----+
           |            |      key  |            |
key +------>   CIPHER   |       +   |   CIPHER   |
           |            |       +--->            |
           +------------+           +------------+
                 |                         |
                 |                         |
                 |                         |
                 v                         v
plaintext  +-->(XOR)       plaintext +-->(XOR)
(P0)             +         (P1)            +
                 |                         |
                 v                         v
             ciphertext (C0)       ciphertext (C1)

The initialization vector is reused every time so we have:

  • C0 = E(key, IV) ⨁ P0
  • C1 = E(key, IV) ⨁ P1
  • ...
  • Cn = E(key, IV) ⨁ Pn

If we find a Pj, where j is an integer, such that Pj = null then we have Cj = E(key, IV). We can then reuse Cj to get the plaintext from the ciphertext :

  1. Cn = E(key, IV) ⨁ Pn
  2. Cn ⨁ Cj = E(key, IV) ⨁ Pn ⨁ E(key, IV)
  3. Cn ⨁ Cj = Pn

Now we can ask ourselves, where can I find a Cj such that Pj = null in our code ? Well the code defines two ciphertexts :

  • c1_null which consists of 16 bytes
  • c2 which consists of 80 bytes (which is 5 blocks of 16 bytes).

What if we tried c1_null ⨁ c2_i to see what happens ?

Exploiting the vulnerability

Here is the Kotlin code I wrote to xor c1_null with c2

fun main() {
    var i = 0
    var xorResult = ByteArray(Compute.c2.size) { 0 }
    while (i < Compute.c2.size) {
        var j = 0
        while (j < Compute.c1_null.size) {
            xorResult[i + j] = Compute.c1_null[j] xor Compute.c2[i + j]
            j++
        }
        i += 16
    }
    println("xor = " + xorResult.toString(Charsets.UTF_8))
}

Here is the result displayed:

xor = Congrats! Dont re-use AES CTR counters ;) Secret Code is: 2mkfmh2r0hkake_m123456

Let's hash the flag to verify that it matches the password we were looking for in the beginnning:

echo -n "2mkfmh2r0hkake_m123456" | sha256sum
6152587ede8a26f53fd391b055d4de501ee8b2497fe74f8fd69f2c72e2f3e37a  -

It does ! We found our password.