FlappyFlopper
HackTheBox
$ ls tags/ techniques/
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 codeassets/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 RVAsscript.json- Method addresses and ScriptString mappingsstringliteral.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:
- Allocates a string array of 28 elements
- Loads individual characters from GOT (Global Offset Table) entries
- Assigns them to
flag[0]throughflag[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 Offset | Character | Used at indices |
|---|---|---|
| 0x2d8 | H | 0 |
| 0x308 | T | 1 |
| 0x320 | B | 2 |
| 0x2c0 | { | 3 |
| 0x348 | F | 4, 10 |
| 0x328 | l | 5, 11, 25 |
| 0x300 | 4 | 6 |
| 0x2c8 | p | 7, 13, 14 |
| 0x310 | y | 8 |
| 0x2f8 | _ | 9, 17, 20 |
| 0x340 | 0 | 12, 18, 22 |
| 0x2d0 | 3 | 15, 26 |
| 0x318 | r | 16 |
| 0x2e8 | n | 19 |
| 0x330 | M | 21 |
| 0x2e0 | b | 23 |
| 0x338 | 1 | 24 |
| 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.soandglobal-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
- [reverse][free]Arno— HackTheBox
- [reverse][free]CubeMadness2— HackTheBox
- [gamepwn][free]LightningFast— HackTheBox
- [gamepwn][free]CubeMadness1— hackthebox
- [gamepwn][free]StayInTheBoxCorp— HackTheBox