reversefreemedium

FlappyFlopper

HackTheBox

$ ls tags/ techniques/
il2cpp_metadata_extractionarm64_disassemblystatic_data_extractionelf_relocation_parsinggot_table_analysis

FlappyFlopper — HackTheBox

Description

No one has got 10000 score yet! Are you able to do so?

English summary: An Android APK (Flappyflopper.arm64-v8a.apk) is provided, which is a Flappy Bird clone built with Unity IL2CPP. The goal is to achieve a score of 10000 to get the flag, but the intended solution is to reverse engineer the IL2CPP binary to extract the flag directly from the static data.

Analysis

The APK is a Unity game compiled with IL2CPP (Intermediate Language to C++), which compiles C# code to native ARM64 code. Key files extracted from the APK:

  • lib/arm64-v8a/libil2cpp.so - The compiled native code
  • assets/bin/Data/Managed/Metadata/global-metadata.dat - IL2CPP metadata (version 29)

Using Il2CppDumper to recover class and method signatures revealed a Score class with a suspicious static flag field:

public class Score : MonoBehaviour // TypeDefIndex: 7209 { // Fields private static string[] flag; // 0x0 - the flag stored as char array! public Font myFontAaset; // 0x20 public Text scoreText; // 0x28 private int score; // 0x30 // Methods private void Start() { } // RVA: 0x9AD55C public void ScoreUp() { } // RVA: 0x9ACDD4 private void UpdateScoreText() { } // RVA: 0x9AD568 public void .ctor() { } // RVA: 0x9AD6E0 private static void .cctor() { } // RVA: 0x9AD6EC - initializes flag[] }

The static constructor .cctor() at RVA 0x9AD6EC is responsible for initializing the flag array. This is where the flag characters are assigned.

Solution

Step 1: APK Extraction

unzip Flappyflopper.arm64-v8a.apk -d apk_extracted/

Step 2: IL2CPP Metadata Recovery

DOTNET_ROLL_FORWARD=LatestMajor dotnet Il2CppDumper.dll \ apk_extracted/lib/arm64-v8a/libil2cpp.so \ apk_extracted/assets/bin/Data/Managed/Metadata/global-metadata.dat \ il2cpp_output/

This generates:

  • dump.cs - Reconstructed C# class definitions with RVAs
  • script.json - Method addresses and ScriptString mappings
  • stringliteral.json - String literals with addresses

Step 3: Disassembly of Score.cctor()

Using radare2 to disassemble the static constructor at offset 0x9AD6EC:

r2 -A libil2cpp.so [0x00000000]> s 0x9AD6EC [0x009ad6ec]> pdf

The function:

  1. Allocates a string array of 28 elements
  2. Loads individual characters from GOT (Global Offset Table) entries
  3. Assigns them to flag[0] through flag[27] in a specific order

Step 4: Resolving GOT Entries via ELF Relocations

GOT entries are zero in the static file (filled at runtime by the dynamic linker). To resolve them statically, we parse the .rela.dyn ELF section to find the addend values which point to actual string data addresses.

#!/usr/bin/env python3 """ FlappyFlopper IL2CPP Flag Extractor Parses ELF relocations to resolve GOT entries and extract flag characters """ import struct import json def parse_elf_relocations(binary_path): """Parse .rela.dyn section to get GOT -> string address mappings""" with open(binary_path, 'rb') as f: data = f.read() # Find .rela.dyn section (ELF64 parsing) # Section header offset at 0x28, section count at 0x3C e_shoff = struct.unpack('<Q', data[0x28:0x30])[0] e_shnum = struct.unpack('<H', data[0x3C:0x3E])[0] e_shstrndx = struct.unpack('<H', data[0x3E:0x40])[0] relocations = {} # Parse sections to find .rela.dyn for i in range(e_shnum): sh_offset = e_shoff + i * 64 sh_type = struct.unpack('<I', data[sh_offset+4:sh_offset+8])[0] # SHT_RELA = 4 if sh_type == 4: sec_offset = struct.unpack('<Q', data[sh_offset+24:sh_offset+32])[0] sec_size = struct.unpack('<Q', data[sh_offset+32:sh_offset+40])[0] # Parse RELA entries (24 bytes each) for j in range(0, sec_size, 24): entry = data[sec_offset+j:sec_offset+j+24] r_offset = struct.unpack('<Q', entry[0:8])[0] r_addend = struct.unpack('<q', entry[16:24])[0] relocations[r_offset] = r_addend return relocations def load_script_strings(script_json_path): """Load ScriptString mappings from Il2CppDumper output""" with open(script_json_path, 'r') as f: data = json.load(f) # Map address -> string value strings = {} for entry in data.get('ScriptString', []): addr = entry['Address'] value = entry['Value'] strings[addr] = value return strings # GOT base address (from disassembly) GOT_BASE = 0x1569000 # GOT offsets for each character (from ARM64 disassembly of .cctor) # Format: (GOT_offset, array_index) got_mappings = [ (0x2d8, 0), # H (0x308, 1), # T (0x320, 2), # B (0x2c0, 3), # { (0x348, 4), # F (0x328, 5), # l (0x300, 6), # 4 (0x2c8, 7), # p (0x310, 8), # y (0x2f8, 9), # _ (0x348, 10), # F (reused) (0x328, 11), # l (reused) (0x340, 12), # 0 (0x2c8, 13), # p (reused) (0x2c8, 14), # p (reused) (0x2d0, 15), # 3 (0x318, 16), # r (0x2f8, 17), # _ (reused) (0x340, 18), # 0 (reused) (0x2e8, 19), # n (0x2f8, 20), # _ (reused) (0x330, 21), # M (0x340, 22), # 0 (reused) (0x2e0, 23), # b (0x338, 24), # 1 (0x328, 25), # l (reused) (0x2d0, 26), # 3 (reused) (0x2f0, 27), # } ] # Resolve and print flag relocations = parse_elf_relocations('libil2cpp.so') strings = load_script_strings('il2cpp_output/script.json') flag = [''] * 28 for got_offset, idx in got_mappings: got_addr = GOT_BASE + got_offset if got_addr in relocations: string_addr = relocations[got_addr] if string_addr in strings: flag[idx] = strings[string_addr] print(''.join(flag))

Step 5: Key Character Mappings

From the relocation analysis:

GOT OffsetCharacterUsed at indices
0x2d8H0
0x308T1
0x320B2
0x2c0{3
0x348F4, 10
0x328l5, 11, 25
0x30046
0x2c8p7, 13, 14
0x310y8
0x2f8_9, 17, 20
0x340012, 18, 22
0x2d0315, 26
0x318r16
0x2e8n19
0x330M21
0x2e0b23
0x338124
0x2f0}27

Reconstructing in order: H T B { F l 4 p y _ F l 0 p p 3 r _ 0 n _ M 0 b 1 l 3 }

Flag

HTB{Fl4py_Fl0pp3r_0n_M0b1l3}

Key Indicators

Use this technique when:

  • Android APK contains libil2cpp.so and global-metadata.dat (Unity IL2CPP game)
  • Static string arrays or fields visible in Il2CppDumper output
  • Flag appears to be constructed character-by-character in static constructor
  • GOT entries show zero values in static analysis (require relocation parsing)
  • ARM64 disassembly shows repeated loads from GOT table with different offsets

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups