This sample uses the same app as Runtime Location Capture with a Deceptive Purpose String. The app binary is signed with the com.apple.developer.healthkit entitlement, which allows the app to request user authorization for HealthKit access. This dummy app does not need the information provided by such entitlement for its functionality. Indeed, the Swift code does not import HealthKit, instantiate HKHealthStore, or request access to HealthKit data types.
This runtime demo traces representative HealthKit APIs associated with the com.apple.developer.healthkit entitlement while exercising the app and verifies if the related APIs are called.
// 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.importUIKitimportCoreLocationstructMastgTest{staticvarlocationCapture:LocationCapture?staticfuncmastgTest(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()letfileURL=saveLocation(locationCapture?.capturedLocation)completion(buildResult(fileURL:fileURL,location:locationCapture?.capturedLocation))locationCapture=nil}}locationCapture?.onDenied={letdir=FileManager.default.urls(for:.documentDirectory,in:.userDomainMask)[0]leturl=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: - PrivateprivatestaticfuncpresentCountdown(seconds:Int,onFinish:@escaping()->Void){DispatchQueue.main.async{guardletscene=UIApplication.shared.connectedScenes.firstas?UIWindowScene,letwindow=scene.windows.first(where:\.isKeyWindow),letrootVC=window.rootViewControllerelse{onFinish();return}vartopVC=rootVCwhileletp=topVC.presentedViewController{topVC=p}letalert=UIAlertController(title:"3s Timer started",message:"Please wait… \(seconds)s",preferredStyle:.alert)topVC.present(alert,animated:true)varremaining=secondslettimer=Timer.scheduledTimer(withTimeInterval:1.0,repeats:true){tinremaining-=1ifremaining>0{alert.message="Please wait… \(remaining)s"}else{t.invalidate()alert.dismiss(animated:true,completion:onFinish)}}RunLoop.main.add(timer,forMode:.common)}}privatestaticfuncsaveLocation(_location:CLLocation?)->URL{letdir=FileManager.default.urls(for:.documentDirectory,in:.userDomainMask)[0]leturl=dir.appendingPathComponent("location_capture.txt")letcontent:Stringifletloc=location{letfmt=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)returnurl}privatestaticfuncbuildResult(fileURL:URL,location:CLLocation?)->String{ifletloc=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 capturefinalclassLocationCapture:NSObject,CLLocationManagerDelegate{privateletmanager=CLLocationManager()private(set)varcapturedLocation:CLLocation?varonAuthorized:(()->Void)?varonDenied:(()->Void)?overrideinit(){super.init()manager.delegate=selfmanager.desiredAccuracy=kCLLocationAccuracyKilometermanager.requestWhenInUseAuthorization()}funcstop(){manager.stopUpdatingLocation()}funclocationManagerDidChangeAuthorization(_manager:CLLocationManager){switchmanager.authorizationStatus{case.authorizedWhenInUse,.authorizedAlways:manager.startUpdatingLocation()onAuthorized?()case.denied,.restricted:onDenied?()default:break}}funclocationManager(_manager:CLLocationManager,didUpdateLocationslocations:[CLLocation]){capturedLocation=locations.last}}
12345678
<?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>
Make sure you have Frida (iOS) installed on your machine and the frida-server running on the device.
Run run_frida.sh to spawn the app with Frida.
Tap the Start button to exercise the sample flow.
Stop the script by pressing Ctrl+C.
12345
#!/bin/bashset-euopipefail
# Extract the entitlements from the app's main binary (see @MASTG-TECH-0111).
rabin2-OC"../MASTG-DEMO-0126/MASTestApp"|sed-n'1,/<\/plist>/p'>"entitlements_reversed.plist"
console.log("\n[*] Starting HealthKit entitlement-backed API tracing...\n");functionprintBacktrace(context,maxLines){console.log("\nBacktrace:");constbacktrace=Thread.backtrace(context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress);for(leti=0;i<Math.min(maxLines,backtrace.length);i++){console.log(backtrace[i]);}}functiondescribeObjCObject(value){if(value.isNull()){return"nil";}try{returnnewObjC.Object(value).toString();}catch(e){returnvalue.toString();}}functionrecordCall(name,context,details){console.log("\n[+] "+name+" called");if(details!==null&&details!==undefined&&details!==""){console.log(" "+details);}printBacktrace(context,6);}functionhookMethod(className,selector,displayName,detailCallback,leaveCallback){constklass=ObjC.classes[className];if(!klass){console.log("[-] Failed to hook "+displayName+": class not found");returnfalse;}constmethod=klass[selector];if(!method){console.log("[-] Failed to hook "+displayName+": method not found");returnfalse;}try{Interceptor.attach(method.implementation,{onEnter(args){constcontext=this.context;constdetails=detailCallback?detailCallback(args):null;recordCall(displayName,context,details);},onLeave(retval){if(leaveCallback){leaveCallback(retval);}}});}catch(e){console.log("[-] Failed to hook "+displayName+": "+e);returnfalse;}console.log("[*] Hooked "+displayName);returntrue;}if(ObjC.available){if(!ObjC.classes.HKHealthStore){console.log("[-] Failed to hook HKHealthStore: class not found");}else{hookMethod("HKHealthStore","+ isHealthDataAvailable","HKHealthStore.isHealthDataAvailable()",null,(retval)=>console.log(" Return value: "+retval.toInt32()));hookMethod("HKHealthStore","- requestAuthorizationToShareTypes:readTypes:completion:","HKHealthStore.requestAuthorization(toShare:read:completion:)",(args)=>"typesToShare="+describeObjCObject(args[2])+", typesToRead="+describeObjCObject(args[3]),null);hookMethod("HKHealthStore","- authorizationStatusForType:","HKHealthStore.authorizationStatus(for:)",(args)=>"type="+describeObjCObject(args[2]),(retval)=>console.log(" Return value: "+retval.toInt32()));hookMethod("HKHealthStore","- getRequestStatusForAuthorizationToShareTypes:readTypes:completion:","HKHealthStore.getRequestStatusForAuthorization(toShare:read:completion:)",(args)=>"typesToShare="+describeObjCObject(args[2])+", typesToRead="+describeObjCObject(args[3]),null);hookMethod("HKHealthStore","- executeQuery:","HKHealthStore.execute(_:)",(args)=>"query="+describeObjCObject(args[2]),null);hookMethod("HKHealthStore","- saveObject:withCompletion:","HKHealthStore.save(_:withCompletion:)",(args)=>"object="+describeObjCObject(args[2]),null);hookMethod("HKHealthStore","- saveObjects:withCompletion:","HKHealthStore.save(_:withCompletion:) [array]",(args)=>"objects="+describeObjCObject(args[2]),null);}}else{console.log("[-] Objective-C runtime is not available.");}console.log("\n[*] Hook setup finished. Interact with the app to trigger HealthKit API calls.\n");
The test case fails because the app is signed with the com.apple.developer.healthkit entitlement, but the exercised runtime flow does not show any HealthKit API use such as HKHealthStore. The sample app does not present any health, fitness, or wellness feature that would justify enabling HealthKit.