//Angry Birds CTF (Mobile)
Challenge hint: “Those who get a huge, huge score brag about it a lot.”
Flag format: CTF{...}
>TL;DR
- The provided file is an xdelta (VCDIFF) patch targeting a specific Angry Birds APK.
- Reconstruct the patched APK using
xdelta3+ the correct base APK. - Inspect the APK → find a custom class
GoogleConnectServicethat posts the score to a server. - That class hardcodes an HMAC secret and the submission URL.
- Forge a “huge score” request with the correct signature and the server returns the flag.
Flag:
CTF{19dd81704a17f45c28dbd185843d7041835860064796438a0b0cf9e0c3363b24}
>1) What files we are given
Workspace content:
angrybirds_ctf.xdelta
Running file angrybirds_ctf.xdelta shows:
VCDIFF binary diff
That means this is not the APK itself; it’s a delta patch.
Key observation: the patch names the exact base APK
Looking at the first bytes/strings inside the patch (example command):
head -c 64 angrybirds_ctf.xdelta | xxd
strings -a -n 6 angrybirds_ctf.xdelta | head
We see the target filename embedded:
... com.rovio.angrybirds_7.9.3-22679300_minAPI16(armeabi-v7a,x86)(nodpi)_apkmirror.com.apk ...
That’s huge: the patch explicitly tells us which original APK we must supply to reconstruct the final challenge APK.
This is a common CTF pattern: shipping only a diff saves space and forces basic reverse/forensics.
>2) Reconstruct the challenge APK
2.1 Install required tool
We need xdelta3 to apply VCDIFF patches.
sudo apt-get update
sudo apt-get install -y xdelta3
2.2 Download the correct base APK
The embedded name points to APKMirror. I used APKMirror’s site to locate the Angry Birds Classic 7.9.3 (22679300) download.
Notes:
curlgot HTTP 403 at first using default UA; using a browser-like User-Agent worked.
Example:
UA='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
# (One way) navigate via APKMirror pages and extract the final download.php?id=...&key=...
curl -L -A "$UA" -o base.apk \\
'<https://www.apkmirror.com/wp-content/themes/APKMirror/download.php?id=425278&key=9077ba7b5e17593ee35b70d4d3c3bc7488466192>'
In my run, I saved it with the same long filename as in the xdelta header:
com.rovio.angrybirds_7.9.3-22679300_minAPI16(armeabi-v7a,x86)(nodpi)_apkmirror.com.apk
2.3 Apply the patch
Now reconstruct the real challenge APK:
xdelta3 -d -f \\
-s 'com.rovio.angrybirds_7.9.3-22679300_minAPI16(armeabi-v7a,x86)(nodpi)_apkmirror.com.apk' \\
angrybirds_ctf.xdelta \\
angrybirds_ctf.apk
file angrybirds_ctf.apk
At this point, you should have a valid APK (a ZIP archive).
>3) Reverse the APK to find the scoring logic
3.1 Decode resources/smali
I used apktool because it’s quick and always available on Linux:
apktool d -f angrybirds_ctf.apk -o decoded
3.2 Search for CTF markers
A fast win in many mobile CTFs is searching for CTF, flag, etc.
grep -R --line-number -I -i 'ctf\\|flag' decoded | head
This revealed an extremely suspicious constant:
"super_secret_key_ctf"
in:
decoded/smali/com/googleconnect/GoogleConnectService.smali
That’s the pivot.
>4) Understand what the app does (the “brag” hint)
The hint says: huge score and brag.
Looking for where this new service is called:
grep -R --line-number -I 'GoogleConnectService;->syncScore' decoded/smali | head
It’s invoked from Facebook score sharing logic:
decoded/smali/com/rovio/rcs/socialnetwork/FacebookService.smali
The app takes the share text (the brag message) and sends it to GoogleConnectService.syncScore(...).
4.1 The important constants
Opening decoded/smali/com/googleconnect/GoogleConnectService.smali shows:
SECRET = "super_secret_key_ctf"URL_STR = "<http://185.213.240.231:54321/submit_score>"
…and a helper that computes HmacSHA256 and hex-encodes it.
This is a classic mobile mistake: shipping secrets client-side.
>5) Interact with the backend
Before forging requests, I tested the endpoint behavior:
curl -i <http://185.213.240.231:54321/submit_score>
# => 405 + "use POST with JSON"
curl -i -H 'Content-Type: application/json' -d '{}' <http://185.213.240.231:54321/submit_score>
# => 400 + "missing fields"
So it’s a JSON POST endpoint that validates required fields.
>6) Figure out the required schema + signature
Because I couldn’t directly decompile the anonymous inner class easily (tooling quirks + inlining), I used a black-box strategy:
- Keep sending JSON until the error changes from
missing fields→invalid signature. - That tells you you’ve supplied all required fields but your HMAC is wrong.
From probing, I determined:
- Required: an identity field called
user - Required:
score - Required:
sig
Then I tried different candidate HMAC payload formats:
HMAC(secret, score)HMAC(secret, user)HMAC(secret, user + score)HMAC(secret, f"{user}:{score}")✅
The winning one is:
$$\text{sig} = \mathrm{HMAC_SHA256}(\text{SECRET}, \text{user} + ":" + \text{score})$$
When you send a huge score with that signature, the server returns the flag.
>7) Solver code
7.1 The exact probing snippet used (ad‑hoc)
This is the snippet I used to test multiple payload formats:
import time, hmac, hashlib, requests
url='<http://185.213.240.231:54321/submit_score>'
secret=b'super_secret_key_ctf'
user='noigel'
score='1000000000000'
def hm(msg):
return hmac.new(secret, msg.encode(), hashlib.sha256).hexdigest()
payloads=[
('score', score),
('user', user),
('user+score', user+score),
('user:score', f"{user}:{score}"),
('user|score', f"{user}|{score}"),
('score:user', f"{score}:{user}"),
]
s=requests.Session()
for label, msg in payloads:
body={'user': user, 'score': score, 'sig': hm(msg)}
r=s.post(url,json=body,timeout=(10,15))
print(label, r.status_code, r.text.strip()[:200])
time.sleep(10)
7.2 Clean final solver script
A cleaner script is provided as solve.py:
python3 solve.py --user noigel --score 1000000000000
It prints the flag.
>8) Why this approach / how I got the idea
This solution is basically “follow the money”:
- The hint says bragging about a huge score → look for score sharing / networking.
- The file is an xdelta patch, so the real app is reconstructed via VCDIFF.
- Mobile apps frequently include:
- hardcoded endpoints
- hardcoded secrets
- naive request signing
So the winning plan is:
- Rebuild the APK (xdelta)
- Grep for secrets / URLs
- Identify signature algorithm
- Recreate the request and get the flag
>9) References
- Xdelta / VCDIFF overview: https://xdelta.org/
- RFC 3284 (VCDIFF): https://www.rfc-editor.org/rfc/rfc3284
- APK decompilation:
- Apktool: https://ibotpeaches.github.io/Apktool/
- JADX: https://github.com/skylot/jadx
- HMAC-SHA256:
- Python
hmacdocs: https://docs.python.org/3/library/hmac.html - Python
hashlibdocs: https://docs.python.org/3/library/hashlib.html
>One-line flag retrieval
If you only want the flag quickly:
python3 - <<'PY'
import hmac,hashlib,requests
secret=b'super_secret_key_ctf'
user='noigel'
score='1000000000000'
sig=hmac.new(secret, f"{user}:{score}".encode(), hashlib.sha256).hexdigest()
print(requests.post('<http://185.213.240.231:54321/submit_score>', json={'user':user,'score':score,'sig':sig}).text)
PY