DEV Community

The Hackers Meetup Nagpur
The Hackers Meetup Nagpur

Posted on • Edited on

Why Some Android Games Resist Naïve Reverse Engineering

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

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Contents:

libapp.so
libflutter.so
Enter fullscreen mode Exit fullscreen mode

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?

  1. Frida (or any binary instrumentation tool)
  2. 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"
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
zipalign -v -p 4 .\king_rebuilt.apk .\king_aligned.apk
Enter fullscreen mode Exit fullscreen mode

You should see:

Verification successful
Enter fullscreen mode Exit fullscreen mode
apksigner sign \
  --ks debug.keystore \
  --ks-pass pass:android \
  --key-pass pass:android \
  --out king_signed.apk \
  king_aligned.apk
Enter fullscreen mode Exit fullscreen mode

Using run-as

Start adb shell and run:

run-as fr.odrevet.kingdomino_score_count
Enter fullscreen mode Exit fullscreen mode

Directory listing:

app_flutter
cache
code_cache
files
Enter fullscreen mode Exit fullscreen mode

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

  1. Flutter apps are extremely resilient to beginner-level hacks.
  • Is this possible with Frida? Maybe. Yes.
  1. 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)