8KSec - FreeFall Game
My solution to 8KSec iOS mobile application exploitation challenge

iOS Runtime Manipulation: Submitting Impossible Scores in a Flutter Game
This article walks through a runtime manipulation attack against FreeFall, an iOS game built with Flutter. The objective is to submit an impossible leaderboard score without altering the application binary, resigning the IPA, or using Flutter-specific modification frameworks like reFlutter.
Instead, we will rely entirely on static native reversing and runtime dynamic instrumentation using Ghidra and Frida.
Challenge Overview
The objective of this challenge is to perform a runtime manipulation attack against an iOS Flutter game called FreeFall and submit impossible leaderboard scores without modifying the application binary.
The challenge explicitly requires:
- No static patching
- No IPA modification
- No binary resigning
- Strictly runtime manipulation
The final solution was achieved using Ghidra and Frida without relying on Flutter-specific tools such as reFlutter.
1. Investigating Info.plist
The first step was reviewing the application's Info.plist file for debug flags, insecure ATS (App Transport Security) settings, hidden endpoints, feature toggles, and environment variables.
Nothing particularly useful was discovered here. Since this challenge focuses on runtime manipulation, the interesting logic was expected to exist inside the application binaries rather than configuration files.
[!NOTE] Analyze configuration bundles first, but expect the core logic to reside inside the compiled binary framework.
Step 1 Screenshot:
2. Initial Mach-O Analysis
Initial string analysis against the main Runner Mach-O binary did not reveal much useful information. This is expected for Flutter applications because Runner is usually only a thin native bootstrap binary.
The actual Flutter and Dart application logic resides inside:
Payload/Runner.app/Frameworks/App.framework/App
Therefore, the investigation quickly shifted toward the Flutter framework binaries.
Step 2 Screenshot:
3. Reversing Flutter Apps with Traditional iOS Techniques
Although FreeFall is built using Flutter, this challenge can still be solved using traditional iOS reverse engineering techniques. Many Flutter reversing writeups rely heavily on reFlutter, Dart snapshot extraction, and Dart pseudo-code reconstruction, but none of that was necessary here.
By treating the application like a standard iOS application and analyzing the native bridge layer, it became possible to:
- Identify Flutter-to-native communication channels.
- Observe database transactions.
- Intercept score submission logic.
- Manipulate runtime values directly.
[!IMPORTANT] Key Insight: Flutter applications still rely on Objective-C runtime APIs, native method channels, SQLite, and Keychain storage, which remain fully accessible through dynamic instrumentation.
Step 3 Screenshot:
4. Reverse Engineering Approach
The reverse engineering process focused on the Runner binary, Flutter framework binaries, Objective-C bridge calls, and SQLite operations. After shifting focus to Payload/Runner.app/Frameworks/App.framework/App, the internal game logic became significantly more visible.
Running string analysis against the Flutter application binary revealed multiple high-value indicators:
submitScore/insertScore/leaderboard/score DESCrawInsert/txnRawInsertCREATE TABLE tokens/security_tokens.dbtoken = ? AND expiry > ?
These strings confirmed that the application relied heavily on local SQLite storage, client-side score handling, and local token validation. The presence of sqflite_darwin.framework also confirmed that the Flutter application was using SQLite through the Flutter Sqflite plugin.
More importantly, discovering Flutter bridge-related functionality strongly suggested that score submission eventually crossed the Objective-C runtime through Flutter method channels. At this point, using Flutter-specific decompilation tools such as reFlutter was no longer necessary because the vulnerable flow could already be understood through static analysis, runtime instrumentation, and Objective-C method hooking.
`
Step 4 Screenshot:
![]()
5. Dynamic Runtime Manipulation with Frida
Instead of patching the application or reconstructing Dart code, the solution relied entirely on runtime instrumentation using Frida.
The key idea was to hook the Flutter bridge method responsible for communication between Dart and native iOS code:
+[FlutterMethodCall methodCallWithMethodName:arguments:]
By intercepting this method, it became possible to observe Flutter method calls, inspect database operations, identify leaderboard insertions, and modify the score dynamically before insertion.
Step 5 Screenshot:
The following Frida script hooks the Flutter bridge and replaces the submitted score with 13371337 on the fly:
'use strict';
if (!ObjC.available) {
console.log("Objective-C runtime unavailable");
} else {
const NSNumber = ObjC.classes.NSNumber;
const NSMutableDictionary = ObjC.classes.NSMutableDictionary;
const NSMutableArray = ObjC.classes.NSMutableArray;
const target = ObjC.classes.FlutterMethodCall[
"+ methodCallWithMethodName:arguments:"
];
console.log("[+] Hooking Flutter bridge...");
Interceptor.attach(target.implementation, {
onEnter(args) {
try {
const methodName = ObjC.Object(args[2]).toString();
console.log("\n====================");
console.log("[Flutter] " + methodName);
const dict = ObjC.Object(args[3]);
if (dict) {
console.log(dict.toString());
}
/*
Detect leaderboard insertion
*/
if (methodName === "insert") {
const originalArgs = dict.objectForKey_("arguments");
if (!originalArgs) return;
console.log("[+] Leaderboard insertion intercepted");
const modified = NSMutableArray.array();
for (let i = 0; i < originalArgs.count(); i++) {
const item = originalArgs.objectAtIndex_(i);
/*
index 1 == score
*/
if (i === 1) {
console.log("[+] Original score: " + item.toString());
modified.addObject_(NSNumber.numberWithInt_(13371337));
console.log("[+] Score replaced");
} else {
modified.addObject_(item);
}
}
const mutable = dict.mutableCopy();
mutable.setObject_forKey_(modified, "arguments");
args[3] = mutable;
console.log("[+] Injection complete");
}
} catch (e) {
console.log("[-] Error: " + e);
}
}
});
}




