Blog section
Writeup

TraceTheMap: Winning an iOS Location Challenge by Patching Distance Checks

An iOS dynamic instrumentation writeup for TraceTheMap, covering Core Location delegate discovery, live CLLocation tracing, distance calculation hooks, and forcing marker collection with Frida.

8kSeciOS SecurityFridaCore LocationDynamic Instrumentation

TraceTheMap is a Swift-based iOS location challenge. The application places five hidden map markers within a 1 km radius and awards 100 points for each marker collected.

TraceTheMap challenge overview

The objective was to make the application display the win page by collecting all five markers:

You Win!
You've collected all 5 flags.

The app only awards a marker when the player is within 50 meters of it. It also includes anti-spoofing and behavioral checks, so blindly spoofing GPS coordinates is not the cleanest path. Instead, I used Frida to understand and patch the app's runtime location-distance checks.

Objective

Score 500 points by making the application believe the current location is within range of all five hidden markers.

Final Result

You Win!
You've collected all 5 flags.

There is no flag string for this challenge. The goal is to reach the win page.

Environment

The challenge was solved on a jailbroken iPhone with Frida. I first identified the target process with:

frida-ps -U -ai
TraceTheMap process discovery

The relevant output showed:

PID   Name          Identifier
1644  TraceTheMap   com.8ksec.TraceTheMap

The running process was confirmed with:

frida-ps -U | grep -i Trace
TraceTheMap process confirmation

Discovering Location-Related Methods

The first step was to identify how the application receives location updates. The discovery script enumerated Objective-C classes and methods containing selectors related to location handling.

for (const className in ObjC.classes) {
  const cls = ObjC.classes[className];

  try {
    if (!cls.$ownMethods) continue;

    cls.$ownMethods.forEach(m => {
      if (m.includes('locationManager') || m.includes('didUpdateLocations')) {
        console.log('[candidate]', className, m);
      }
    });
  } catch (_) {}
}

The script was executed against the running app:

frida -U -n TraceTheMap -l trace_location.js
Tracing location-related Objective-C methods
TraceTheMap LocationManager candidate

The important app-specific candidate was:

[candidate] TraceTheMap.LocationManager - locationManager:didUpdateLocations:

This confirmed that GPS updates were received through a custom class named TraceTheMap.LocationManager.

Hooking the Location Manager

After identifying the delegate method, I hooked it and inspected the live CLLocation objects received by the application.

const targetClass = 'TraceTheMap.LocationManager';
const targetMethod = '- locationManager:didUpdateLocations:';
const cls = ObjC.classes[targetClass];

Interceptor.attach(cls[targetMethod].implementation, {
  onEnter(args) {
    const locations = new ObjC.Object(args[3]);
    console.log('[locations]', locations.toString());
    console.log('[count]', locations.count());
  }
});

The script was executed with:

frida -U -n TraceTheMap -l hook_location_manager.js
Hooking TraceTheMap LocationManager updates

The hook confirmed that the app was consuming normal Core Location updates through standard CLLocation objects.

Enumerating the Custom Location Manager

The custom location manager class was then enumerated to see whether marker or score-related methods were exposed directly.

const targetClass = 'TraceTheMap.LocationManager';
const cls = ObjC.classes[targetClass];

console.log('[+] Found class:', targetClass);
cls.$ownMethods.forEach(m => {
  console.log(m);
});
frida -U -n TraceTheMap -l enum_location_manager.js
Enumerating TraceTheMap LocationManager methods

The class only exposed initialization and the location update callback:

- init
- locationManager:didUpdateLocations:
- .cxx_destruct

That meant the scoring logic was probably performed downstream after location updates were received. Instead of hunting every Swift score method, the cleaner target was the shared distance calculation used by the app.

Tracing Distance Calculations

The challenge rule was simple:

Collect a marker when distance <= 50 meters

On iOS, distance between two CLLocation objects is commonly calculated with -[CLLocation distanceFromLocation:]. I hooked this method and printed both locations involved in each distance check.

const CLLocation = ObjC.classes.CLLocation;
const method = CLLocation['- distanceFromLocation:'];

Interceptor.attach(method.implementation, {
  onEnter(args) {
    this.selfLoc = new ObjC.Object(args[0]);
    this.otherLoc = new ObjC.Object(args[2]);
    console.log('[self]', this.selfLoc.toString());
    console.log('[other]', this.otherLoc.toString());
  },
  onLeave(retval) {
    console.log('[raw retval]', retval);
  }
});
frida -U -n TraceTheMap -l trace_distance.js
Tracing CLLocation distanceFromLocation calls

The output showed repeated distance checks between the current location and five different marker locations, matching the challenge requirement to collect five markers. That made CLLocation -distanceFromLocation: the best patch point.

Patching distanceFromLocation:

The app's scoring condition was effectively:

if distance <= 50 meters:
    collect marker
    add 100 points

Rather than spoofing GPS coordinates, I replaced the method that returns the distance. -[CLLocation distanceFromLocation:] returns a CLLocationDistance, which is a double. The replacement returned 10.0, safely below the 50 meter threshold.

const CLLocation = ObjC.classes.CLLocation;
const method = CLLocation['- distanceFromLocation:'];

const replacement = new NativeCallback(function (self, _cmd, otherLocation) {
  console.log('[+] distanceFromLocation called -> fake 10.0m');
  return 10.0;
}, 'double', ['pointer', 'pointer', 'pointer']);

Interceptor.replace(method.implementation, replacement);
frida -U -n TraceTheMap -l force_distance.js
Forcing CLLocation distanceFromLocation to return 10 meters

Each call returned 10.0m, causing every marker check to pass as if the player were within collection range.

Win Page

After the patched distance checks completed, the application awarded all five markers and displayed the win page:

You Win!
You've collected all 5 flags.

Demo Video

Scripts Used

  • trace_location.js discovered app-specific location delegate methods.
  • hook_location_manager.js inspected live CLLocation objects received by the app.
  • enum_location_manager.js enumerated methods on TraceTheMap.LocationManager.
  • trace_distance.js identified repeated calls to CLLocation -distanceFromLocation:.
  • force_distance.js replaced the distance calculation and returned 10.0 meters.

Final Thoughts

This challenge was a good example of choosing a stable patch point instead of fighting the app's anti-spoofing behavior. By tracing the location pipeline and patching the shared distance calculation, every marker check passed without needing to simulate realistic movement across the map.