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.

Leave a Reply