Reverse engineering an Android app part 2 — Following the breadcrumbs

Last time we cracked open the APK and got readable-ish Java source from JADX. 24,744 classes, most with single-letter names. Now we need to find the API signing logic — a needle in a very obfuscated haystack.

Start with what you can grep

Obfuscation renames classes and methods, but it can’t rename string literals or crypto algorithm names. So we grep:

grep -r "signature\|HMAC\|hmac" decompiled/ --include="*.java"

This surfaces a handful of hits, including an interceptor class that adds signed parameters to every outgoing API request. Bingo — that’s our entry point.

Tracing the call chain

From the interceptor, we follow the method calls deeper:

graph TD
    A["HTTP Interceptor"] -->|calls| B["Po.c — SignatureEncryptor"]
    B -->|reads fields| C["a: partnerKey
b: algorithmKey
c: algorithmName"] B -->|calculates| D["HMAC signature"] C -->|populated by| E["Secrets.java — JNI bridge"] E -->|loads| F["libsecrets.so — native library"]

The SignatureEncryptor (obfuscated to Po.c) is where the signature gets calculated. JADX shows us the decompiled logic:

public final class c {
    private final String a;  // partnerKey
    private final String b;  // algorithmKey
    private final String c;  // algorithmName

    public final String a(long j) {
        String str = this.a + "|" + j;
        Mac mac = Mac.getInstance(this.c);
        mac.init(new SecretKeySpec(this.b.getBytes(), this.c));
        return Base64.encodeToString(mac.doFinal(str.getBytes()), 2);
    }
}

Every API request gets signed with these query parameters:

?t.p=PARTNER_ID             # hardcoded partner identifier
&t.k=PARTNER_KEY            # included in signature data
&appVersion=12.21.1         # app version from manifest
&s.expires=TIMESTAMP        # now + 1 day, in milliseconds
&signature=SIGNATURE         # Base64(HMAC-SHA1(algorithmKey, partnerKey + "|" + expires))

So the signature is an HMAC-SHA1 of the partner key concatenated with a pipe and the expiry timestamp, using a separate algorithm key as the HMAC secret. Three values we need to extract: the partner key, the algorithm key, and the algorithm name. But where do they come from?

Down the rabbit hole to native code

Tracing the constructor calls back, we find a Secrets.java class with JNI methods:

System.loadLibrary("secrets");

public native String getSignaturePartnerKey();
public native String getSignatureAlgorithmKey();
public native String getSignatureAlgorithmName();

The secrets aren’t in the Java code at all. They’re in libsecrets.so — a compiled native library, XOR-encoded and only decoded at runtime. Static analysis of the .so with radare2 reveals the decoding function, but the XOR scheme is position-dependent with a key derived from SHA256. Reversing it manually would take ages.

The wrong assumption

At this point, based on common Android patterns and the crypto imports we’d seen, we assumed the algorithm would be HmacSHA256. We were wrong — but we wouldn’t find that out until we extracted the actual runtime values. Static analysis can only take you so far.

graph LR
    A["Static analysis"] -->|suggested| B["HmacSHA256 ❌"]
    C["Runtime extraction"] -->|revealed| D["HmacSHA1 ✅"]

Next time: we bring in Frida to read the decoded secrets straight from memory.

Reverse engineering an Android app part 1 — Cracking it open

When a client asks us to build an integration against an API that has no public documentation, sometimes the only option is to go straight to the source — the app itself. We recently needed to understand how a particular Android app signs its API requests, and what followed was a proper detective story in three acts. This is act one: getting the source code.

XAPK is just a zip in a trenchcoat

Modern Android apps don’t ship as a single APK anymore. They come as XAPKs — bundles of split APKs, each serving a specific purpose:

graph TD
    A["app.xapk"] -->|unzip| B["base APK — code + resources"]
    A --> C["config.arm64_v8a.apk — native libs"]
    A --> D["config.en.apk — English resources"]
    A --> E["config.xxhdpi.apk — screen density assets"]

The split keeps downloads smaller — your phone only fetches what it needs. For us, it means an extra extraction step before we can get to the code.

# Rename and extract — it's literally a zip
unzip app.xapk -d xapk-extracted/

The base APK is where the interesting bits live. Inside it: 8 DEX files containing roughly 24,744 classes. That’s a lot of haystacks to search through.

Two tools, two perspectives

We reach for two decompilation tools that serve different purposes.

JADX converts DEX bytecode back into readable Java. It’s our go-to for understanding what the code does:

jadx -d output app.apk --show-bad-code -j 4

The --show-bad-code flag is important — when JADX can’t perfectly decompile something, it’ll show its best attempt as a comment rather than silently skipping it.

Apktool decodes resources and produces smali — a human-readable assembly for Dalvik. We need this when we want to modify and rebuild the app later:

apktool d app.apk -o output_dir
graph LR
    A["APK"] --> B["JADX"]
    A --> C["Apktool"]
    B --> D["Java source — for reading"]
    C --> E["Smali + resources — for modifying"]

But then everything’s obfuscated

Here’s what JADX gives us for a typical class:

public final class c {
    private final String a;
    private final String b;
    private final String c;

    public c(String str, String str2, String str3) {
        this.a = str;
        this.b = str2;
        this.c = str3;
    }
}

Single-letter class names, single-letter fields. This is R8 obfuscation at work — Google’s code shrinker renames everything to minimise APK size (and conveniently makes reverse engineering harder). Not all is lost though — string literals, annotation-preserved names, and network DTOs often survive the obfuscation. That’s where we start pulling the thread.

In the next post, we’ll trace through the obfuscated code to find where the app hides its API signing secrets.

Android apps SSL Unpinning

Every now and then a client comes to us with an interesting challenge: they need to see what an Android app is actually sending over the wire. Maybe it’s a security audit, maybe they’re debugging an API integration, or maybe they just want to understand what data is leaving their devices. SSL pinning makes this tricky — the app refuses to trust anything other than its own bundled certificate. Here’s how we get around that.

What you’ll need

An Android device — this can be a physical phone, but it doesn’t have to be. Projects like Bliss OS let you run full Android on an x86 virtual machine, which is honestly more convenient for this kind of work — no cables, easy snapshots, and you can throw the VM away when you’re done. We’ve had good results running Bliss OS on Proxmox. Setting up an Android x86 VM is a topic for another day, but it’s worth knowing the option exists.

Software:

  • ADB (Android Debug Bridge)
  • Frida and Objection
  • patch-apk script for repackaging
  • Your proxy’s root certificate installed on the device

Patching the APK

The patch-apk script does the heavy lifting — it decompiles the APK, injects the Frida gadget, and repackages it. Sometimes things won’t build cleanly though. We’ve hit cases where special characters in resource files trip up apktool during reassembly. If that happens, hunt down the offending characters and sanitise them before rebuilding.

Intercepting traffic

Once you’ve got the patched APK installed on the device:

  1. Set up a proxy server with MITM support on your machine — we’ve used Fiddler but mitmproxy works too
  2. Configure the device to use your machine as its proxy
  3. Launch the patched app — it’ll appear stuck on a black screen initially, that’s normal

Now connect to it:

objection explore

Once Objection confirms it’s connected and linked to the instrumented app, the app will unfreeze. Then disable SSL pinning:

android sslpinning disable

Use the app as normal and watch the decrypted traffic flow through your proxy. All the API calls, payloads, headers — everything that was previously hidden behind the pinned certificate is now visible in plain text.