Angry Birds (VianuCTF 2025) — Android HMAC Score Forgery

A patched Angry Birds APK with a hidden score-submission server. The description says "huge, huge score" — turns out that's the entire hint. Reverse the HMAC secret from the decompiled code, forge a 109 score, get the flag.

1. Getting the patched APK

The challenge provides an angrybirds_ctf.xdelta patch file, not the APK itself. The description says the patch applies to Angry Birds Classic — so the first step is tracking down the original Angry Birds Classic 7.9.3 APK and applying the patch:

xdelta3 -d -s AngryBirdsClassic_7.9.3.apk angrybirds_ctf.xdelta angrybirds_patched.apk

The result is a functional APK that installs and runs exactly like the original — no obvious sign anything was changed. That's intentional.

2. Decompiling with JADX

I ran JADX on the patched APK to get readable Java source:

jadx -d angrybirds_jadx angrybirds_patched.apk

Most of the code is the standard Angry Birds codebase. I filtered for classes that weren't in the original — and GoogleConnectService stood out immediately. It's not a real Google service; the name is chosen to blend in.

3. Finding GoogleConnectService

GoogleConnectService.java is the only class added by the patch. It has one relevant method: syncScore(String score). The call site is in the Facebook share-score flow — whenever the user taps "Share Score", the app calls syncScore with the current score before doing anything else.

The method builds a JSON POST and sends it to a hardcoded endpoint:

http://185.213.240.231:54321/submit_score

The request has three fields: user, score, and sig. The server validates the signature before responding with anything useful.

The signing key is hardcoded in the same class:

private static final String SECRET = "super_secret_key_ctf";

4. The HMAC scheme

The signature is computed as HMAC-SHA256 over the string "user:score", keyed with the hardcoded secret:

message  = user + ":" + score
signature = HMAC-SHA256(key="super_secret_key_ctf", msg=message)

The server reconstructs the same signature from the received user and score fields and compares. If the HMAC matches and the score is high enough, it responds with the flag.

The description hint ("huge, huge score") points directly at the threshold — a score of 1,000,000,000 is what triggers the flag response.

5. Forging the score — Python path

With the secret known, generating a valid signature is straightforward:

import hmac
import hashlib
import requests
import json

SECRET = "super_secret_key_ctf"
URL    = "http://185.213.240.231:54321/submit_score"

user  = "player"
score = 1000000000

message   = f"{user}:{score}"
signature = hmac.new(
    SECRET.encode(),
    message.encode(),
    hashlib.sha256
).hexdigest()

payload = {"user": user, "score": score, "sig": signature}
r = requests.post(URL, headers={"Content-Type": "application/json"}, data=json.dumps(payload))
print(r.text)

The server responds with the flag.

6. Alternative: Frida path

If the goal is to go through the app rather than forge the request directly, two hooks are needed: one to replace the score with 1 billion before it's sent, and one to read the server response before the app discards it.

Java.perform(function() {
    // Intercept the HTTP response to read the flag
    let HttpURLConnection = Java.use("java.net.HttpURLConnection");
    HttpURLConnection.getInputStream.implementation = function() {
        let stream = this.getInputStream();
        let Scanner = Java.use("java.util.Scanner");
        let response = Scanner.$new(stream).useDelimiter("\\A").next();
        console.log("[flag] " + response);
        let ByteArrayInputStream = Java.use("java.io.ByteArrayInputStream");
        return ByteArrayInputStream.$new(response.getBytes());
    };

    // GoogleConnectService loads after the main activity — wait before hooking
    setTimeout(function() {
        let GoogleConnect = Java.use("com.googleconnect.GoogleConnectService");
        GoogleConnect.syncScore.implementation = function(score) {
            this.syncScore("1000000000");
        };
    }, 3000);
});
The setTimeout delay is necessary — GoogleConnectService is loaded after the main activity initializes. The hook needs to be in place before the user taps "Share Score", so 3 seconds is enough time in practice.

The Python approach is cleaner and doesn't require a rooted device or Frida setup. Either way, the core is the same: the secret is in the decompiled source, the signing scheme is HMAC-SHA256 with a hardcoded key, and the score threshold is in the challenge description.