HackerOne H1-702 – Challenge 2

Description: Looks like this app is all locked up. Think you can figure out the combination?

HackerOne h1-702

Install the apk with adb

				
					adb install -r challenge2_h1-702.apk
				
			

And then, decompile with apktool

				
					apktool d challenge2_h1-702.apk
				
			

We can see a PIN app, which have six numbers combination.
So, we can simply try the 1.000.000 combinations (C= 10⁶ = 1.000.000) or look the source code.

Open jadx (GUI Version) for analyze the code.
The package name is com.hackerone.mobile.challenge2
And the unique activity is the MainActivity.
But, we have some extra classes like SecretBoxPinLockViewIndicatorDots

Here’s the main code of our interest

				
					[...]
[...]
public class MainActivity extends AppCompatActivity {  
    private static final char[] hexArray;  
    private byte[] cipherText;  
    IndicatorDots mIndicatorDots;  
    PinLockView mPinLockView;  
    String TAG = "PinLock";  
    private PinLockListener mPinLockListener = new PinLockListener() { // from class: com.hackerone.mobile.challenge2.MainActivity.1  
        @Override // com.andrognito.pinlockview.PinLockListener  
        public void onComplete(String str) {  
            Log.d(MainActivity.this.TAG, "Pin complete: " + str);  
            byte[] key = MainActivity.this.getKey(str);  
            Log.d("TEST", MainActivity.bytesToHex(key));  
            try {  
                Log.d("DECRYPTED", new String(new SecretBox(key).decrypt("aabbccddeeffgghhaabbccdd".getBytes(), MainActivity.this.cipherText), StandardCharsets.UTF_8));  
            } catch (RuntimeException e) {  
                Log.d("PROBLEM", "Unable to decrypt text");  
                e.printStackTrace();  
            }  
        }  
  
        @Override // com.andrognito.pinlockview.PinLockListener  
        public void onEmpty() {  
            Log.d(MainActivity.this.TAG, "Pin empty");  
        }  
  
        @Override // com.andrognito.pinlockview.PinLockListener  
        public void onPinChange(int i, String str) {  
            Log.d(MainActivity.this.TAG, "Pin changed, new length " + i + " with intermediate pin " + str);  
        }  
    };  
  
    public native byte[] getKey(String str);  
  
    public native void resetCoolDown();
[...]
[...]
				
			

The code use native code for decrypt the flag. We can see the getKey function that work with the pin code.

We can hook the onComplete function for inspect how it work.
The PinLockListener use an anonymous class.

Anonymous classes generate a class name like <aClassName>$<id>.
The id start from 1 until the last anonymous class. In this case we can find the anonymous class in com.hackerone.mobile.challenge2.MainActivity$1.

We can fin this with

				
					tree smali/com/hackerone/mobile/challenge2
				
			

And see

				
					smali/com/hackerone/mobile/challenge2
├── BuildConfig.smali
├── MainActivity$1.smali
[...]
				
			

Let’s hook the onComplete with frida.
Here’s the script

				
					Java.perform(function () {
    const Listener = Java.use('com.hackerone.mobile.challenge2.MainActivity$1');

    Listener.onComplete.implementation = function (key) {
        console.log("[*] Hooked onComplete called with key: " + key);

        function generateAndTest(count, prefix) {
            if (count === 0) {
                this.onComplete(prefix);
                console.log("Trying key: " + prefix);
                return;
            }

            for (let i = 0; i < 10; i++) {
                generateAndTest.call(this, count - 1, prefix + i.toString());
            }
        }

        console.log("[*] Starting brute force...");
        generateAndTest.call(this, 6, "");

        console.log("[*] Brute force completed.");
    };
});
				
			

we can see that this work, but also every 50 attempts, there are an wait. This can make more difficult to bruteforce the pin.
So, the next challenge is that we need bypass the protection.
Which is another native code in resetCoolDown(); function.

This functions is like an ok, start again the process. So, we need call it every 50 attempts. Like

				
					      if (i % 50 == 0) {
        this.this$0.value.resetCoolDown();
      }
				
			

So now, the actual code for hook onComplete looks like

				
					Java.perform(function () {
    const Listener = Java.use('com.hackerone.mobile.challenge2.MainActivity$1');

    Listener.onComplete.implementation = function (key) {
        console.log("[*] Hooked onComplete called with key: " + key);

        function generateAndTest(count, prefix) {
            if (count === 0) {
                this.onComplete(prefix);
                console.log("Trying key: " + prefix);
                return;
            }


            for (let i = 0; i < 10; i++) {
                generateAndTest.call(this, count - 1, prefix + i.toString());

                if (i % 50 == 0) {
                	this.this$0.value.resetCoolDown();
             	}
            }
        }

        console.log("[*] Starting brute force...");
        generateAndTest.call(this, 6, "");

        console.log("[*] Brute force completed.");
    };
});
				
			

And we can see that the bruteforce run without stoppers.
The final step is know what is the correct PIN, right? We need check of some way the correct pin.

Looking in logcat if the code work

				
					adb logcat -c && adb logcat | grep -E "complete|TEST|PROBLEM"
				
			

We can see the attempts.

Since the onComplete functions doesn’t make sense (this just return logcat content), we need inspect the SecretBox.decrypt()

				
					public byte[] decrypt(byte[] bArr, byte[] bArr2) {  
        Util.checkLength(bArr, 24);  
        byte[] prependZeros = Util.prependZeros(16, bArr2);  
        byte[] zeros = Util.zeros(prependZeros.length);  
        NaCl.sodium();  
        Util.isValid(Sodium.crypto_secretbox_xsalsa20poly1305_open(zeros, prependZeros, prependZeros.length, bArr, this.key), "Decryption failed. Ciphertext failed verification");  
        return Util.removeZeros(32, zeros);  
    }
				
			

This return the decrypted data if it could be decrypted successfully.
Let’s hook the method.
We can implement this code

				
					    let success = false;

    SecretBox.decrypt.implementation = function (bArr, bArr2) {
        let ret = "";
        try {
            ret = this.decrypt(bArr, bArr2);
            success = true;
            console.log("[*] Decryption successful! Found flag: " + JavaString.$new(ret));
        } catch (ex) {
            success = false;
            ret = Java.array('byte', []);
        }
        return ret;
    };
    ```
For work with the method.
Also, this code
```javascript
Listener.onComplete.implementation = function (key) {
        console.log("[*] Hooked onComplete called with key: " + key);

        const self = this;

        function generateAndTest(count, prefix) {
            if (success) return;

            if (count === 0) {
                console.log("[*] Trying key: " + prefix);

                const ret = self.onComplete(prefix);
                if (success) {
                    console.log("[*] The correct PIN is: " + prefix);
                    return ret;
                }
                return;
            }

            for (let i = 0; i < 10; i++) {
                if (success) break;

                generateAndTest(count - 1, prefix + i.toString());

                if (i % 50 === 0 &#038;&#038; !success) {
                    console.log("[*] Resetting cooldown...");
                    self.this$0.value.resetCoolDown();
                }
            }
        }
				
			

Will modify the call to onComplete() for check if this is correct.
I’ll start from the beginning, this will take a long of time, but, I’ll get the dinner while the code is running.

The final code look like

				
					Java.perform(function () {
    const Listener = Java.use('com.hackerone.mobile.challenge2.MainActivity$1');
    const SecretBox = Java.use('org.libsodium.jni.crypto.SecretBox');
    const JavaString = Java.use('java.lang.String');

    let success = false;

    SecretBox.decrypt.implementation = function (bArr, bArr2) {
        let ret = "";
        try {
            ret = this.decrypt(bArr, bArr2);
            success = true;
            console.log("[*] Decryption successful! Found flag: " + JavaString.$new(ret));
        } catch (ex) {
            success = false;
            ret = Java.array('byte', []);
        }
        return ret;
    };

    Listener.onComplete.implementation = function (key) {
        console.log("[*] Hooked onComplete called with key: " + key);

        const self = this;

        function generateAndTest(count, prefix) {
            if (success) return;

            if (count === 0) {
                console.log("[*] Trying key: " + prefix);

                const ret = self.onComplete(prefix);
                if (success) {
                    console.log("[*] The correct PIN is: " + prefix);
                    return ret;
                }
                return;
            }

            for (let i = 0; i < 10; i++) {
                if (success) break;

                generateAndTest(count - 1, prefix + i.toString());

                if (i % 50 === 0 &#038;&#038; !success) {
                    console.log("[*] Resetting cooldown...");
                    self.this$0.value.resetCoolDown();
                }
            }
        }

        console.log("[*] Starting brute force...");
        generateAndTest(6, "");

        if (!success) {
            console.log("[*] Brute force completed without finding the correct PIN.");
        }
    };
});
				
			

And the code works!!

				
					[*] Resetting cooldown...
[*] Trying key: 918251
[*] Trying key: 918252
[*] Trying key: 918253
[*] Trying key: 918254
[*] Trying key: 918255
[*] Trying key: 918256
[*] Trying key: 918257
[*] Trying key: 918258
[*] Trying key: 918259
[*] Trying key: 918260
[*] Resetting cooldown...
[*] Trying key: 918261
[*] Trying key: 918262
[*] Trying key: 918263
[*] Trying key: 918264
[*] Decryption successful! Found flag: flag{wow_yall_called_a_lot_of_func$}
[*] The correct PIN is: 918264
[Pixel 2::PID::3422 ]->
[Pixel 2::PID::3422 ]->
				
			

PIN: 918264
Flag: flag{wow_yall_called_a_lot_of_func$}

I hope you found it useful (:

Leave a Reply

Your email address will not be published. Required fields are marked *