Audience
Beginners, students, CTF players, and AppSec enthusiasts.
Scope & Intent
This article documents a practical reverse-engineering attempt on a small Android game.
The goal is not to present a complete exploit, but to show how tooling, architecture, and app design affect what is realistically modifiable.
1. Why Reverse Engineering APKs Matters
Reverse engineering is the process of understanding what a system does without having access to its original source code.
In Android security, reverse engineering is used for:
- AppSec audits (logic flaws, exposed components)
- Malware analysis
- CTFs & crackmes
- Understanding proprietary apps
Reverse engineering isn’t limited to software. It applies to:
- Hardware
- Firmware
- Embedded devices
- Automotive systems
- Mobile applications
In this article, we focus on Android APK reverse engineering.
2. What Readers Will Learn by the End
- Common beginner mistakes in Android reverse engineering
- How app architecture (Flutter vs native) changes attack surfaces
- How to recognize dead ends early
3. What Is an APK?
An APK (Android Package) is the installable executable format for Android, similar to .exe on Windows.
Technically, an APK is just a ZIP archive.
If you unzip it, you’ll find:
Key Components
classes.dex
- Contains DEX bytecode executed by Android Runtime (ART)
- Primary target for reverse engineers
AndroidManifest.xml
- Defines:
- Entry points (activities)
- Permissions
- Exported components
- First file attackers inspect
res/ & resources.arsc
- UI layouts, strings, images
- Hardcoded secrets often leak here
META-INF/
- App signatures & certificates
- Any modification breaks integrity unless re-signed
4. Static Analysis
Let’s do something interesting: reverse-engineer a small Android game and make it behave the way we want.
Spoiler: I thought this would be simple.
The plan was straightforward: grab a small Android game, reverse it, tweak a few things, and “win” at will.
Easy, right?
Famous last words.
Target APK
- APK: https://f-droid.org/en/packages/fr.odrevet.kingdomino_score_count/
- Downloaded as:
KingDomino.apk
Since this is from F-Droid, we can safely assume the app is non-obfuscated and cleanly built: perfect for a beginner reverse-engineering attempt.
a) Manifest Analysis
First stop: AndroidManifest.xml.
I unpacked the APK using apktool:
apktool d KingDomino.apk -o king
The manifest is always worth inspecting first: not because it tells you how the app works, but because it tells you what the app is allowed to do: permissions, exported components, entry points, and intent filters.
Minimal example of what we usually look for:
<uses-permission android:name="android.permission.INTERNET"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
This immediately answers two questions:
- What capabilities does the app request?
- Where does execution start?
What This App’s Manifest Shows
In this case, the manifest was refreshingly boring:
- A single launcher activity
- No suspicious permissions
- No exported services or receivers doing anything shady
Relevant part (trimmed for clarity):
<application
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="Kingdomino Score">
<activity
android:name="com.example.kingdomino_score_count.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data android:name="flutterEmbedding" android:value="2"/>
</application>
Nothing crazy here: just a game asking for local storage and declaring a main activity.
b) Decompiled Code (jadx)
Next step: jadx-gui.
Normally, this is where you start tracing logic from MainActivity, following method calls, hunting for score calculations or state variables.
…and that was it.
No logic.
No methods.
No state.
My brain hit pause.
First Red Flag
Scrolling through the imports and parent classes, something felt off. I kept seeing references to:
io.flutter.embedding.android.FlutterActivity
Flutter?
Wait… what?
This wasn’t a normal Java/Kotlin game. The Java/Kotlin code wasn’t the game at all, it was just a wrapper.
Exploring around different main files in root directories:
Signature panel
Shows signer name, signatures, and hashes.
If this were a malicious APK, this would be a starting point to match hashes with known malware.Summary panel
Shows what jadx has found after decoding the APK.
It clearly states native libs, a strong indicator of a Flutter build.
jadx also shows kotlin-tooling-metadata, which is an easy trap to fall into.
Seeing Kotlin build metadata does not mean the app logic is written in Kotlin.
Flutter apps still use:
- Gradle
- Kotlin plugins
- Android wrappers
This metadata only tells you how the Android shell was built, not where the game logic lives.
Where the Logic Actually Lives
The real game logic wasn’t in Java or Kotlin at all.
It was written in Dart, compiled ahead-of-time into a native shared library: libapp.so.
Everything I normally do hunting strings, patching methods in Java was useless.
The logic was literally hidden behind Flutter’s engine.
At that moment, I realized this was going to be a lesson in why Flutter apps are so resilient to beginner hacks.
My plan to just “patch the logic” hit a wall.
5. Dynamic Analysis
Dart is AOT (Ahead-Of-Time compiled).
The game logic may not appear anywhere obvious in the decoded app but it exists inside lib/.
Example directory:
C:\king\lib\arm64-v8a
Contents:
libapp.so
libflutter.so
These are shared object files.
We can’t just “Ghidra” over them in any meaningful way.
Ghidra will only show stripped native code with little semantic meaning.
Flutter AOT compiles Dart directly into native code.
What’s the Workaround?
- Frida (or any binary instrumentation tool)
- A partial hack
We’ll talk about the hack here.
Since we downloaded the APK from F-Droid (important), we can patch the Manifest to enable debugging.
Enabling Debugging
Open AndroidManifest.xml and add:
android:debuggable="true"
Example:
<application
android:appComponentFactory="androidx.core.app.CoreComponentFactory"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="Kingdomino Score"
android:name="android.app.Application"
android:debuggable="true">
Make sure this is inside <application> and above the first <activity> tag.
Rebuild, Align, and Sign
java -jar apktool_2.12.1.jar b .\king -o .\king_rebuilt.apk
zipalign -v -p 4 .\king_rebuilt.apk .\king_aligned.apk
You should see:
Verification successful
apksigner sign \
--ks debug.keystore \
--ks-pass pass:android \
--key-pass pass:android \
--out king_signed.apk \
king_aligned.apk
Using run-as
Start adb shell and run:
run-as fr.odrevet.kingdomino_score_count
Directory listing:
app_flutter
cache
code_cache
files
If the game were persistent, saved scores or preferences would appear here.
But this game isn’t persistent.
Files may also be visible in phone storage, but real-time scores are never stored.
Dead End? Or…
Why This Failed
- Flutter apps are extremely resilient to beginner-level hacks.
- Is this possible with Frida? Maybe. Yes.
- The game itself is non-persistent
- Scores are calculated and rendered at runtime
- Nothing is written to disk
- No preferences, database, or file to patch
Even if this were a pure Java/Kotlin app, there would still be nothing to modify.
If high scores or coins were stored locally, modifying them via run-as would have been trivial.
Here, there was simply nothing to persist.
The Real Problem
- Game logic lives in native AOT Dart code
- Logic operates entirely in memory
- No persistence
- No exposed state
To modify behavior, you’d need:
- Runtime instrumentation
- Memory hooks
- A middleman between logic and rendering
This is not feasible on a non-rooted device without advanced tooling.
Final Takeaways
Before touching jadx.
Before hunting strings.
Before dreaming about patches.
Know what you’re reversing.
Next time:
- I’ll check for Flutter first.
- And maybe I’ll bring Frida.
Stay tuned.
Written By: Akanksha Sawant
From: THM Nagpur Core Team
Disclaimer
This article documents a practical reverse-engineering attempt and the conclusions drawn from it. While care has been taken to ensure technical accuracy, some interpretations may be incomplete or context-dependent. Constructive corrections or clarifications are welcome.
Top comments (0)