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);
});
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.