Skip to content

MASTG-DEMO-0126: Runtime Location Capture with a Deceptive Purpose String

Download MASTG-DEMO-0126 IPA Open MASTG-DEMO-0126 Folder Build MASTG-DEMO-0126 IPA

Sample

The app shows a 3-second countdown popup when the Start button is tapped. That is the only user-visible feature: there is no map, no search, and no other location-based functionality anywhere in the UI.

Despite that, the app requests location access and collects coarse location coordinates (kilometer-level accuracy) in the background for the full duration of the countdown. When the countdown ends, the coordinates are written to location_capture.txt in the app's Documents directory. The user sees a "3s Timer started" popup with no mention of location at any point.

The Info.plist declares a single purpose string, NSLocationWhenInUseUsageDescription, with the text "We use your location to show you nearby content and recommendations." This is deceptive: it describes a feature that does not exist in the app, and the purpose string shown in the system prompt is inconsistent with the only observable behavior (a countdown).

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// SUMMARY: An app that shows a 3-second countdown popup when Start is tapped. The only
// user-visible feature is the countdown; there is no map, search, or any other
// location-based feature in the UI. Despite that, the app silently requests location access
// and records the GPS coordinates to a file while the countdown runs. The
// NSLocationWhenInUseUsageDescription purpose string is deceptive: it claims a
// "nearby content" feature that the app does not provide.

import UIKit
import CoreLocation

struct MastgTest {
    static var locationCapture: LocationCapture?

    static func mastgTest(completion: @escaping (String) -> Void) {
        // FAIL: [MASTG-TEST-0361] The app requests location access and collects GPS
        // coordinates while showing a plain countdown popup. The
        // NSLocationWhenInUseUsageDescription purpose string ("to show you nearby content
        // and recommendations") is deceptive: the app has no location-based feature and
        // never shows any content related to the user's location.
        locationCapture = LocationCapture()

        locationCapture?.onAuthorized = {
            // Start the countdown only after permission is granted so GPS collection
            // is running for the full 3 seconds.
            presentCountdown(seconds: 3) {
                locationCapture?.stop()
                let fileURL = saveLocation(locationCapture?.capturedLocation)
                completion(buildResult(fileURL: fileURL, location: locationCapture?.capturedLocation))
                locationCapture = nil
            }
        }

        locationCapture?.onDenied = {
            let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let url = dir.appendingPathComponent("location_capture.txt")
            try? "Location access denied.".write(to: url, atomically: true, encoding: .utf8)
            completion("Location access denied.\n\nSaved to: location_capture.txt")
            locationCapture = nil
        }
    }

    // MARK: - Private

    private static func presentCountdown(seconds: Int, onFinish: @escaping () -> Void) {
        DispatchQueue.main.async {
            guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                  let window = scene.windows.first(where: \.isKeyWindow),
                  let rootVC = window.rootViewController else {
                onFinish(); return
            }
            var topVC = rootVC
            while let p = topVC.presentedViewController { topVC = p }

            let alert = UIAlertController(title: "3s Timer started", message: "Please wait… \(seconds)s", preferredStyle: .alert)
            topVC.present(alert, animated: true)

            var remaining = seconds
            let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { t in
                remaining -= 1
                if remaining > 0 {
                    alert.message = "Please wait… \(remaining)s"
                } else {
                    t.invalidate()
                    alert.dismiss(animated: true, completion: onFinish)
                }
            }
            RunLoop.main.add(timer, forMode: .common)
        }
    }

    private static func saveLocation(_ location: CLLocation?) -> URL {
        let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let url = dir.appendingPathComponent("location_capture.txt")
        let content: String
        if let loc = location {
            let fmt = ISO8601DateFormatter()
            content = """
            Latitude:  \(loc.coordinate.latitude)
            Longitude: \(loc.coordinate.longitude)
            Altitude:  \(String(format: "%.1f", loc.altitude)) m
            Accuracy:  \(String(format: "%.1f", loc.horizontalAccuracy)) m
            Timestamp: \(fmt.string(from: loc.timestamp))
            """
        } else {
            content = "Location not available (no fix received)."
        }
        try? content.write(to: url, atomically: true, encoding: .utf8)
        return url
    }

    private static func buildResult(fileURL: URL, location: CLLocation?) -> String {
        if let loc = location {
            return """
            Countdown completed.

            [!] Location captured in the background:
            Latitude:  \(loc.coordinate.latitude)
            Longitude: \(loc.coordinate.longitude)
            Altitude:  \(String(format: "%.1f", loc.altitude)) m

            Saved to: \(fileURL.lastPathComponent)
            """
        } else {
            return """
            Countdown completed.

            [!] Location capture attempted (no GPS fix received in time).
            Saved to: \(fileURL.lastPathComponent)
            """
        }
    }
}

// MARK: - Location capture

final class LocationCapture: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    private(set) var capturedLocation: CLLocation?
    var onAuthorized: (() -> Void)?
    var onDenied: (() -> Void)?

    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyKilometer
        manager.requestWhenInUseAuthorization()
    }

    func stop() {
        manager.stopUpdatingLocation()
    }

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse, .authorizedAlways:
            manager.startUpdatingLocation()
            onAuthorized?()
        case .denied, .restricted:
            onDenied?()
        default:
            break
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        capturedLocation = locations.last
    }
}
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>We use your location to show you nearby content and recommendations.</string>
</dict>
</plist>

Steps

  1. Use Exploring the App Package to unzip the app package.
  2. Use Retrieving Info.plist Files to retrieve ./Payload/MASTestApp.app/Info.plist and save it as Info.plist in this demo directory.
  3. Use Convert Plist Files to JSON to convert Info.plist to a readable format if needed.
  4. Use Analyzing Info.plist Files to inspect the purpose strings by running run.sh.
  5. Install the app on a device using Installing Apps.
  6. Make sure Frida (iOS) is installed on your machine and frida-server is running on the device.
  7. Run run_frida.sh to spawn the app with Frida ( Method Hooking).
  8. Tap the Start button to trigger the countdown and observe the location access in the background.
  9. Stop the script by pressing Ctrl+C.
1
2
#!/bin/bash
frida -U -f org.owasp.mastestapp.MASTestApp-iOS -l ./script.js -o output_frida.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Hook the location authorization and collection APIs at runtime.
// This script traces (1) the permission request that displays the
// NSLocationWhenInUseUsageDescription purpose string, (2) the start of GPS collection,
// and (3) the stop, which marks the end of the capture window.

console.log("\n[*] Starting Location Access Tracing...\n");

const printBacktrace = (context, maxLines = 8) => {
    console.log("\nBacktrace:");
    let backtrace = Thread.backtrace(context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress);
    for (let i = 0; i < Math.min(maxLines, backtrace.length); i++) {
        console.log(backtrace[i]);
    }
};

if (ObjC.available) {
    // CLLocationManager.requestWhenInUseAuthorization (instance method).
    // Displays NSLocationWhenInUseUsageDescription to the user when status is notDetermined.
    try {
        Interceptor.attach(ObjC.classes.CLLocationManager["- requestWhenInUseAuthorization"].implementation, {
            onEnter(args) {
                console.log("\n[+] CLLocationManager.requestWhenInUseAuthorization called");
                console.log("    Purpose string key: NSLocationWhenInUseUsageDescription");
                printBacktrace(this.context);
            }
        });
        console.log("[*] Hooked CLLocationManager.requestWhenInUseAuthorization");
    } catch (e) {
        console.log("[-] Failed to hook requestWhenInUseAuthorization: " + e);
    }

    // CLLocationManager.startUpdatingLocation (instance method).
    // Reaching this confirms the app actively collects GPS coordinates.
    try {
        Interceptor.attach(ObjC.classes.CLLocationManager["- startUpdatingLocation"].implementation, {
            onEnter(args) {
                console.log("\n[+] CLLocationManager.startUpdatingLocation called");
                console.log("    GPS coordinate collection has started.");
                printBacktrace(this.context);
            }
        });
        console.log("[*] Hooked CLLocationManager.startUpdatingLocation");
    } catch (e) {
        console.log("[-] Failed to hook startUpdatingLocation: " + e);
    }

    // CLLocationManager.stopUpdatingLocation (instance method).
    // Marks the end of the capture window (called when the countdown ends).
    try {
        Interceptor.attach(ObjC.classes.CLLocationManager["- stopUpdatingLocation"].implementation, {
            onEnter(args) {
                console.log("\n[+] CLLocationManager.stopUpdatingLocation called");
                console.log("    GPS coordinate collection has stopped.");
                printBacktrace(this.context);
            }
        });
        console.log("[*] Hooked CLLocationManager.stopUpdatingLocation");
    } catch (e) {
        console.log("[-] Failed to hook stopUpdatingLocation: " + e);
    }
}

console.log("\n[*] Hooks installed. Tap the Start button to trigger the countdown and location capture.\n");

Observation

The output reveals the purpose string declared in the app's Info.plist file.

output_purpose_strings.txt
1
  "NSLocationWhenInUseUsageDescription" => "We use your location to show you nearby content and recommendations."

The only declared purpose string is:

  • NSLocationWhenInUseUsageDescription

The Frida script output reveals the location APIs reached at runtime while the countdown runs.

output_frida.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[*] Starting Location Access Tracing...

[*] Hooked CLLocationManager.requestWhenInUseAuthorization
[*] Hooked CLLocationManager.startUpdatingLocation
[*] Hooked CLLocationManager.stopUpdatingLocation

[*] Hooks installed. Tap the Start button to trigger the countdown and location capture.


[+] CLLocationManager.requestWhenInUseAuthorization called
    Purpose string key: NSLocationWhenInUseUsageDescription

Backtrace:
0x1010cfef0 MASTestApp.debug.dylib!LocationCapture.init()
0x1010cff38 MASTestApp.debug.dylib!@objc LocationCapture.init()
0x1010cc534 MASTestApp.debug.dylib!LocationCapture.__allocating_init()
0x1010cc2a0 MASTestApp.debug.dylib!static MastgTest.mastgTest(completion:)
0x1010d26e0 MASTestApp.debug.dylib!closure #1 in closure #1 in closure #1 in ContentView.body.getter
0x1920fd550 SwiftUI!0x150c550 (0x18cec1550)
0x191994c30 SwiftUI!0xda3c30 (0x18c758c30)
0x1919986bc SwiftUI!0xda76bc (0x18c75c6bc)

[+] CLLocationManager.startUpdatingLocation called
    GPS coordinate collection has started.

Backtrace:
0x1010cffd4 MASTestApp.debug.dylib!LocationCapture.locationManagerDidChangeAuthorization(_:)
0x1010d0134 MASTestApp.debug.dylib!@objc LocationCapture.locationManagerDidChangeAuthorization(_:)
0x199549ccc CoreLocation!-[CLLocationManager onClientEventAuthStatus:]
0x199543b74 CoreLocation!-[CLLocationManager onClientEvent:supportInfo:]
0x1995438e4 CoreLocation!0x108e4 (0x1943078e4)
0x1aa8e0524 LocationSupport!-[CLSilo prepareAndRunBlock:]
0x18d0fe9b8 CoreFoundation!__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
0x18d0ed910 CoreFoundation!__CFRunLoopDoBlocks

[+] CLLocationManager.stopUpdatingLocation called
    GPS coordinate collection has stopped.

Backtrace:
0x1010cca18 MASTestApp.debug.dylib!LocationCapture.stop()
0x1010cc6b4 MASTestApp.debug.dylib!closure #1 in closure #1 in static MastgTest.mastgTest(completion:)
0x1010cef18 MASTestApp.debug.dylib!thunk for @escaping @callee_guaranteed () -> ()
0x18f4a53a0 UIKitCore!-[UIPresentationController transitionDidFinish:]
0x18f9da39c UIKitCore!__56-[UIPresentationController runTransitionForCurrentState]_block_invoke.91
0x18f4f950c UIKitCore!-[_UIViewControllerTransitionContext completeTransition:]
0x18f29af94 UIKitCore!__UIVIEW_IS_EXECUTING_ANIMATION_COMPLETION_BLOCK__
0x18f29ae9c UIKitCore!-[UIViewAnimationBlockDelegate _didEndBlockAnimation:finished:context:]

The runtime trace shows that tapping Start:

  1. Calls CLLocationManager.requestWhenInUseAuthorization, which displays NSLocationWhenInUseUsageDescription to the user. The backtrace links the call to LocationCapture.init(), invoked via LocationCapture.__allocating_init() from MastgTest.mastgTest(completion:).
  2. Calls CLLocationManager.startUpdatingLocation once the user grants permission. The backtrace originates in LocationCapture.locationManagerDidChangeAuthorization(_:), called by CoreLocation after authorization is granted, confirming that coarse location collection begins immediately.
  3. Calls CLLocationManager.stopUpdatingLocation when the countdown ends. The backtrace shows LocationCapture.stop() called from closure #1 in closure #1 in static MastgTest.mastgTest(completion:), which is the onFinish block invoked by UIKit (-[UIPresentationController transitionDidFinish:]) when the countdown alert's dismiss animation completes.

Evaluation

The test case fails because the declared purpose string is deceptive. It tells the user that location is used to show nearby content and recommendations, but the app does not provide any nearby content, recommendations, map, search, or other location-based feature.

The only user-visible feature is a 3-second countdown popup. The runtime trace confirms that location authorization and collection APIs are reached during that countdown flow, so the purpose string is not merely unused or stale. It is shown for reachable location access that is inconsistent with the app's observable behavior.

The user must still grant location permission before the app can access the protected resource. However, the issue remains valid because the authorization prompt is based on an inaccurate explanation, and the observed location access is not justified by the app's visible functionality.