Cylent Security

Cylent Security

8ksec TraceTheMap iOS CTF

Solving the 8ksec TraceTheMap iOS CTF

I've been enjoying solving 8ksec's iOS Application Exploitation Challenges after completing the first half (iOS) of the Practical Mobile Application Exploitation course. These challenges reinforce what I've learned in the course and make learning iOS application reverse engineering fun.

Description

TraceTheMap is an iOS location-based challenge where you must collect 5 hidden map markers scattered within a 1 km radius. Each collectible is worth 100 points—and you need all 500 to win.

Get within 50 meters of each collectible to score.
Sounds simple? Not so fast. While spoofing your GPS might seem like the obvious path, this app comes with a few built-in countermeasures to detect foul play. From unexpected location sanity checks to behavioral traps, it won’t be a walk in the park—even if you fake it.

Solving the CTF

When starting the app, this is the first view:

Opening Screen

Since location is involved, the first thing I did was run frida-trace and filter for classes related to "Location". The following command traces every Objective-C class that contains the word "Location":

frida-trace -U -m "-[*Location* *]" -f com.8ksec.TraceTheMap.WBC6955H89

This produced a lot of output. While reviewing the output, I noticed something interesting repeating: -[CLLocation distanceFromLocation:0x30230cc40]. Based on the challenge description, this seemed to be the right target for further exploration.

I ran frida-trace again, this time filtering on the class method:

frida-trace -U -m "-[CLLocation distanceFromLocation:]" -f com.8ksec.TraceTheMap.WBC6955H89
Instrumenting...                                                        
-[CLLocation distanceFromLocation:]: Loaded handler at "/Users/steve/dev/frida-ios-hooks/__handlers__/CLLocation/distanceFromLocation_.js"
Started tracing 1 function. Web UI available at http://localhost:61631/ 
           /* TID 0x103 */
   481 ms  -[CLLocation distanceFromLocation:0x3016bf470]
   481 ms  -[CLLocation distanceFromLocation:0x3016bf470]
   481 ms  -[CLLocation distanceFromLocation:0x3016bf470]
   481 ms  -[CLLocation distanceFromLocation:0x3016bf470]
   481 ms  -[CLLocation distanceFromLocation:0x3016bf470]
  5939 ms  -[CLLocation distanceFromLocation:0x301643210]
  5939 ms  -[CLLocation distanceFromLocation:0x301643210]
  5939 ms  -[CLLocation distanceFromLocation:0x301643210]
  5939 ms  -[CLLocation distanceFromLocation:0x301643210]
  5939 ms  -[CLLocation distanceFromLocation:0x301643210]
 20975 ms  -[CLLocation distanceFromLocation:0x3016434a0]
 20975 ms  -[CLLocation distanceFromLocation:0x30165e100]
 20975 ms  -[CLLocation distanceFromLocation:0x30165e100]
 20976 ms  -[CLLocation distanceFromLocation:0x30165e100]
 20976 ms  -[CLLocation distanceFromLocation:0x30165e100]

You can see in the script output that a group of five locations repeats periodically. I edited the default handler for this method:

let lastSpoofTime = 0;

defineHandler({
  onEnter(log, args, state) {
    log(`-[CLLocation distanceFromLocation:${args[2]}]`);
  },

  onLeave(log, retval, state) {
    const now = Date.now();
    if (now - lastSpoofTime > 1500) {
      const val = 10.0 + Math.random() * (40.0 - 10.0);
      this.context.d0 = val;
      lastSpoofTime = now;
      log(`-> Spoofed: Distance is now ${val.toFixed(2)} meters!`);
    }
  }
});

The script handler above is the final version that solved the challenge. The first time I ran it, I didn't get any points. I ran some Frida scripts I created to detect anti-debugging but none were detected. Thinking logically about how the developer may have attempted to prevent cheating, I changed a few things.

  1. On line 1, I added lastSpoofTime to track how long it had been between spoofing my location.
  2. On line 9, I added a constant for now to get the current time.
  3. Then I used the if statement to check if had been at least 1.5 seconds since the last time I spoofed my location.
  4. If true, I calculate a random floating point number between 10.0 and 40.0, just in case the app was checking to see if I used static numbers.
  5. this.context.d0 = val: This method returns a double, therefore I need to use the d0 register to return the value.
  6. I reset the lastSpoofTime counter to the current time.

This code ensures that I spoof the location only one time for each interation through the five locations, and that I pause between each spoof attempt. It works and I solved the challenge! (There is no flag in this challenge)

I solved the challenge