Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
AndroidHard

Angry Birds Ctf (Mobile)

CTF writeup for Angry Birds Ctf (Mobile) from Vianu CTF

//Angry Birds CTF (Mobile)

Challenge hint: “Those who get a huge, huge score brag about it a lot.”

Flag format: CTF{...}


>TL;DR

  1. The provided file is an xdelta (VCDIFF) patch targeting a specific Angry Birds APK.
  2. Reconstruct the patched APK using xdelta3 + the correct base APK.
  3. Inspect the APK → find a custom class GoogleConnectService that posts the score to a server.
  4. That class hardcodes an HMAC secret and the submission URL.
  5. 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):

bash

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.

bash

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:

  • curl got HTTP 403 at first using default UA; using a browser-like User-Agent worked.

Example:

bash

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:

bash

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:

bash

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.

bash

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:

bash

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:

bash

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:

  1. Keep sending JSON until the error changes from missing fieldsinvalid signature.
  2. 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:

python

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:

bash

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:

  1. Rebuild the APK (xdelta)
  2. Grep for secrets / URLs
  3. Identify signature algorithm
  4. Recreate the request and get the flag

>9) References


>One-line flag retrieval

If you only want the flag quickly:

bash

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