Obsess over every detail. Ask why it works. Ask why it isn’t built another way.

Today we’re going to do a mix of static and dynamic analyis to extract hidden flags from a vulnerable Android app using Frida’s Java bridge to invoke methods directly at runtime.

Reconnaissance

Loading the APK in JADX, we can see the app only has one exported activity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <SNIP>
    <activity
        android:theme="@style/Theme.FridaTarget.NoActionBar"
        android:label="@string/app_name"
        android:name="io.hextree.fridatarget.MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
</manifest>

Browsing the io.hextree.fridatarget namespace, we find several interesting classes:

io.hextree.fridatarget
  - databinding
  - ui
  - ExampleClass
  - FlagClass        <-- interesting
  - FlagCryptor      <-- interesting
  - LicenseManager   <-- interesting
  - MainActivity

Analyzing FlagClass

Inspecting FlagClass, we can see three methods that each return a decoded flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class FlagClass {
    static String flagFromStaticMethod() {
        return FlagCryptor.decodeFlag("VUtHe24tZmduZ3ZwLXBueXl2YXQtanZndS1zZXZxbn0=");
    }
    public String flagFromInstanceMethod() {
        return FlagCryptor.decodeFlag("VUtHe3FsYW56dnAtcWVidnF9");
    }
    public String flagIfYouCallMeWithSesame(String password) {
        if (password.equalsIgnoreCase("sesame")) {
            return FlagCryptor.decodeFlag("VUtHe2d1ci1xZWJ2cS1sYmhlci15YmJ4dmF0LXNiZX0=");
        }
        return null;
    }
}

The goal is clear, we need to call these methods directly and read the return values.

JADX has a right click option to copy a class as a Frida snippet, which gives you a ready-made JS template. However, pasting it directly into the Frida REPL won’t work because of how the Frida-to-Java bridge operates under the hood.

Java.perform()

The Frida-Java bridge requires that any code interacting with Java classes runs on a thread that is properly attached to the JVM. Calling Java code outside of this context causes errors. Java.perform(fn) solves this by ensuring your function runs on a thread that is correctly attached to the Android Runtime before executing.

Without Java.perform  ,  your code runs on Frida's thread, not attached to JVM, crashes
With Java.perform     ,  Frida ensures proper thread attachment before running your code

Writing the Script

1
2
3
4
5
6
7
8
Java.perform(() => {
    let FlagClass = Java.use("io.hextree.fridatarget.FlagClass");
    let FlagInstance = FlagClass.$new(); // create an instance of FlagClass
    
    console.log(FlagInstance.flagFromStaticMethod());
    console.log(FlagInstance.flagFromInstanceMethod());
    console.log(FlagInstance.flagIfYouCallMeWithSesame("sesame")); // passes the password check
});

Java.use gets a JS wrapper around the Java class. $new() instantiates it, equivalent to calling new FlagClass() in Java. From there we can call its methods directly as if we were the app itself.

Result

1
2
3
4
5
6
❯ frida -U -l script.js FridaTarget
...
Attaching...
HXT{a-static-calling-with-frida}
HXT{dynamic-droid}
HXT{the-droid-youre-looking-for}

All three flags extracted by directly invoking the class methods through Frida’s Java bridge.