FlappyFlopper
FlappyFlopper
Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-24 | Status: Solved Techniques: arm64_disassembly, elf_relocation_parsing, got_table_analysis, il2cpp_metadata_extraction, static_data_extraction
Summary
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 }
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
HackTheBox| ID:20260324_hackthebox_flappyflopper - Tags: game_hacking, android, apk, unity, il2cpp, arm64, metadata_extraction, got_relocation
- Indicators: Unity IL2CPP Android APK, libil2cpp.so in lib/arm64-v8a/, global-metadata.dat in assets/bin/Data/Managed/Metadata/, static string array field in dumped class, GOT entries with zero values requiring relocation parsing
- Source:
20260324_hackthebox_flappyflopper.md
Foothold
Vulnerability / Misconfiguration
- Arm64_disassembly
- Elf_relocation_parsing
- Got_table_analysis
- Il2cpp_metadata_extraction
- Static_data_extraction
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- arm64_disassembly
- elf_relocation_parsing
- got_table_analysis
- il2cpp_metadata_extraction
- static_data_extraction
- Tags: game_hacking, android, apk, unity, il2cpp, arm64, metadata_extraction, got_relocation
Original Writeup
<details><summary>Click to expand original content</summary>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 }
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR