Challenge: Baby Android · CTF: DalCTF 2026 · Category: Reversing / Mobile · Difficulty: Easy

TL;DR

The challenge hands you a single BabyAndroid.apk. The running app only shows some sarcastic “nothing to see here” text — the real flag is split into three pieces scattered across three different locations inside the APK:

  1. A hardcoded field in MainActivity
  2. A string resource in res/values/strings.xml
  3. A property getter in the Compose theme package (ColorKt)

Decompile, grep, concatenate:

1
dalctf{4ndr0id_d3bugg1ng_1s_e4sy}

Recon

We start with exactly one file:

1
2
3
4
5
$ ls -la
-rw-r--r-- 1 kali kali 14860964 Jun  6 21:50 BabyAndroid.apk

$ file BabyAndroid.apk
BabyAndroid.apk: Zip archive data, at least v0.0 to extract, compression method=deflate

An APK is just a ZIP, so the first thing to do is look at what’s inside it. The package metadata tells us this is a stock Jetpack Compose app:

1
2
$ aapt dump badging BabyAndroid.apk | head -1
package: name='com.example.babyandroid' versionCode='1' versionName='1.0' ...

Listing the archive shows a lot of dex (Compose pulls in a huge runtime), a couple of native .so files for androidx.graphics.path, and nothing obviously custom:

1
2
3
4
5
6
7
8
$ unzip -l BabyAndroid.apk | grep -E '\.dex|\.so'
   17705132  classes.dex
   11645868  classes5.dex
    2308296  classes6.dex
     529268  classes2.dex
      20320  classes4.dex      <-- our app code lives here
      10096  lib/arm64-v8a/libandroidx.graphics.path.so
      ...

The tiny classes4.dex (20 KB) is the giveaway — almost everything else is the Compose/AndroidX framework. The application’s own code is small.

Decompiling

For Android, the go-to tool is jadx, which turns the dex bytecode back into readable Java:

1
$ jadx -d jadx_out BabyAndroid.apk

⚠️ Gotcha: jadx resolves a relative APK path against its own install directory (/usr/share/jadx/bin/), so it’ll complain File not found. Pass an absolute path to the APK and output dir and it works fine.

After decompilation, the app’s own package is refreshingly small:

1
2
3
4
5
6
7
8
$ find jadx_out/sources/com/example -type f
.../com/example/babyandroid/MainActivity.java
.../com/example/babyandroid/MainActivityKt.java
.../com/example/babyandroid/ComposableSingletons$MainActivityKt.java
.../com/example/babyandroid/R.java
.../com/example/babyandroid/ui/theme/ColorKt.java
.../com/example/babyandroid/ui/theme/ThemeKt.java
.../com/example/babyandroid/ui/theme/TypeKt.java

Seven files. This is a “Baby” challenge — the flag isn’t going to be behind a custom VM or an obfuscator. It’s going to be sitting in plain sight if we know where to look.

Piece 1 — MainActivity

Opening MainActivity.java immediately pays off:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public final class MainActivity extends ComponentActivity {
    public static final int $stable = 8;
    private final String flag1 = "dalctf{4ndr0id";   // <-- piece 1

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable$default(this, null, null, 3, null);
        ComponentActivityKt.setContent$default(
            this, null,
            ComposableSingletons$MainActivityKt.INSTANCE.m7653getLambda$168778512$app(),
            1, null);
    }
}

There it is: flag1 = "dalctf{4ndr0id". The variable is literally named flag1, which tells us there are more parts (flag2, flag3, …) to find.

Note that flag1 is never actually used or displayed anywhere — it’s a dead field. The UI is rendered entirely from the Compose lambdas, which (as we’ll see) only contain decoy text. This is the whole point of the challenge: the flag lives in the binary, not on the screen.

The red herrings (what the app actually shows)

If you’d installed and run the app instead of reverse engineering it, you’d have seen this — and nothing else. The MainScreen composable (jadx struggles to decompile the full Compose state machine, so we re-run with --show-bad-code) contains only taunts:

1
2
3
4
TextKt.m2817Text4IGK_g("Move along, folks.", ...);
TextKt.m2817Text4IGK_g("have you checked under the hood?", ...);
// and elsewhere:
TextKt.m2817Text4IGK_g("nothing to see here", ...);

“have you checked under the hood?” is the hint — stop poking at the UI and read the bytecode. This matches the challenge’s theme: Android Debugging.

Piece 2 — the string resources

The flag1 naming convention says to hunt for siblings. A quick grep across both the decompiled sources and the resources is the fastest way:

1
2
3
4
5
6
7
$ grep -rn "flag" jadx_out/sources/com/example/ jadx_out/resources/
.../MainActivity.java:    private final String flag1 = "dalctf{4ndr0id";
.../ui/theme/ColorKt.java:    private static final String flag3 = "_1s_e4sy}";
.../resources/AndroidManifest.xml:  android:description="@string/flag2">
.../R.java:   public static int flag2 = 0x7f0f003f;
.../resources/res/values/public.xml:  <public type="string" name="flag2" .../>
.../resources/res/values/strings.xml:  <string name="flag2">_d3bugg1ng_</string>

Two more pieces fall out at once. flag2 is interesting because it’s wired into the AndroidManifest as the activity’s android:description attribute — a perfectly legitimate place to stash a string that never shows up in the UI:

1
<activity ... android:description="@string/flag2">

And in res/values/strings.xml:

1
<string name="flag2">_d3bugg1ng_</string>

So piece 2 = _d3bugg1ng_.

Piece 3 — hiding in the theme

The grep also caught flag3 living in, of all places, the Compose color definitions (ui/theme/ColorKt.java) — right next to Purple80, Pink40, and friends:

1
2
3
4
5
6
7
8
public final class ColorKt {
    ...
    private static final String flag3 = "_1s_e4sy}";   // <-- piece 3

    public static final String getFlag3() {
        return flag3;
    }
}

Piece 3 = _1s_e4sy}.

Assembling the flag

Three pieces, three locations:

PieceWhere it livesValue
flag1MainActivity field (classes4.dex)dalctf{4ndr0id
flag2res/values/strings.xml (manifest description)_d3bugg1ng_
flag3ColorKt.getFlag3() (theme package)_1s_e4sy}

Concatenating in order:

1
dalctf{4ndr0id  +  _d3bugg1ng_  +  _1s_e4sy}

reads as “android debugging is easy”, giving the flag:

1
dalctf{4ndr0id_d3bugg1ng_1s_e4sy}

💡 A note on the underscores. If you concatenate the three stored strings byte-for-byte, flag2 ends with _ and flag3 begins with _, which produces a double underscore: dalctf{4ndr0id_d3bugg1ng__1s_e4sy}. The intended, human-readable flag uses a single underscore (..._d3bugg1ng_1s_e4sy}). If one is rejected by the scoreboard, submit the other — but the single-underscore version is the natural reading and the one that matches the “debugging is easy” sentence.

Flag

1
dalctf{4ndr0id_d3bugg1ng_1s_e4sy}

Takeaways

  • An APK is a ZIP. The flag doesn’t have to be in code — strings.xml, the manifest, assets, and resources are all fair game. Always grep the decompiled resources, not just the Java.
  • Don’t trust the UI. What the app shows you (“nothing to see here”) is often a deliberate distraction. The data is in the binary whether it’s drawn on screen or not.
  • Follow the naming. Finding a field called flag1 is an explicit invitation to go look for flag2 and flag3. A single grep -rn flag over the jadx output solved 2 of the 3 pieces in one shot.
  • Tooling tip: give jadx absolute paths, and use --show-bad-code when a Compose composable refuses to decompile cleanly.

A fitting “Baby” RE challenge: no obfuscation, no anti-debug, no native crypto — just a lesson in where Android stashes strings and a reminder to check under the hood.