Bypassing Disabled Location Services in macOS Mojave and Catalina

Introduction

When I was a core maintainer of osquery (back when it was under the control of Facebook), I was the only one fully focused on macOS things. We each had different areas we tended to operate in and macOS was mine. So when Mojave came out and we started getting reports that the wifi_survey table had started crashing it was up to me to investigate. This is the story of what I found.

User Intent and macOS

To understand why this is even a problem to start with, you need to understand Apple’s philosophy on security, privacy, and user intent (or my interpretation of Apple anyway). Apple products aspire to be simple and do what the user expects, even if that might be counter intuitive to developers. You see this with the photo picker on iOS where you’re expected to request a single user selected picture for use (like Snapchat), not the photo-roll unless you depend on that functionality (like Lightroom). Requesting a single picture requires no entitlements while photo roll access does along with a purpose string. This brings user data exposure down from all pictures to a single user selected picture, a significant privacy win. This is Apple solving a problem many users didn’t even know they had. The Location Services issue is similar in that way.

Divergence of Understanding

macOS High Sierra had a privacy settings panel in System Preferences that allowed and denied apps permission for several things (ironically one of which was Facebook integrations) and I think was a good step forward for user privacy.
Location Services being toggled off entirely is where we start to see what a developer thinks diverge from a typical user.

Developer: Disabling Location Services makes the CoreLocation Framework unavailable or restricted.

End User: Disabling Location Services makes apps unable to physically locate me.

Apple seems quite passionate at addressing these divergences and want to remove as many as possible. What we’ll be talking about here is using a wireless scan to get pin point location accuracy as an unprivileged user, even when location services is disabled.

Getting accurate location from wireless scans is extremely easy and Apple was one of the first companies (that I know of) to bring this to the public’s attention when they launched location abilities on the iPod Touch. These devices lacked GPS and users were concerned when accurate pins started showing up in the Google Maps app. End users didn’t (and still don’t) understand the connection between wifi and the impact it has on their privacy thus Apple took a step towards fixing this in macOS Mojave.

The Mojave Fix

The osquery crashes in Mojave were caused by the wifi_survey table when it tried to generate data for a running query. Tracing the program with LLDB made problem clear: the BSSID field was gone. The data returned from CoreWLAN is an NSDictionary that excepts/crashes upon accessing the non-existent “bssid” key which osquery had always used before. Adding an existence check before this access fixed osquery and it was patched that day.

But the BSSID field being gone is strange so we should try to discover why. It could be a mistake (this was Mojave Beta 1), it could be that it was moved to another API (splitting non essential functionality out for performance or space), or it could be a privacy change (BSSIDs can locate a user). There is no documentation at this point so we’ll need to look into Apple’s code to determine what happened.

The scanForNetworksWithName function in CoreWLAN seems like a reasonable place to start.

This falls through to a larger function, scanForNetworksWithChannels where a lot more happens and a clearer picture starts to form. First notice the XPC references indicating scanning likely happens in another process which returns the data to us.

Further down blockBSSIDAccess which takes no parameters and returns an integer seems promising. When it returns anything other than 0x0 it will remove the BSSID keys from the returned NSDictionary. Whatever it checks it seems likely this was the cause of osquery’s issue. Disassembling the function shows it has a critical mistake as it operates on data it treats as trusted when it isn’t.

What that long line amounts to is if any condition is true, return false, meaning don’t block BSSIDs. So when does this return false?

  • When the bundle identifier is prefixed with com.apple. OR

  • When the process name is coreautomationd OR

  • When the process name is WirelessStress OR

  • When the process name is wifivelocityd OR

  • When Location Services are enabled.

Why Mojave Didn’t Fix The Problem

There are two separate problems here, one is a coding problem and one is a structural problem.

The Structural Problem

Stripping BSSIDs in this function will never work. This is our own process (the XPC call has already returned) and the data is already in our memory space. We could even make the XPC call explicitly and have BSSIDs returned to us directly, without using CoreWLAN. This data needs to be filtered during the XPC call giving us no chance to see the data.

The Coding Problem

We control all of these fields. If we name our process any of those three or have a bundle identifier prefixed with com.apple. we would pass that check and have BSSIDs unstripped when the function returns.

The Proof of Concept

Objective-C has a feature called method swizzling allowing you to easily swap out function implementations at run time with another function of the same prototype. To bypass this check entirely, all anyone has to do is swizzle the mainBundle function to return com.apple.something instead of com.company.something and BSSIDs would be populated even if Location Services was disabled. There is a patched version of the wifi_survey.mm that does this you can compile into osquery to try for yourself on a Mojave system.

Reporting to Apple

I reported this to Apple the second day the Mojave beta was available, June 6th 2018. To date, it has not been fixed in Mojave. Perhaps they are busy, perhaps this is not high priority for them but I do think a year is a bit excessive.
However, in June 2019, Apple released 10.15 Catalina, and guess what. They fixed it!

Except they didn’t.

Why Catalina Didn’t Fix The Problem

Remember, there were two problems and either one would allow you to bypass this protection. They fixed the structural problem, leaving the coding problem mostly intact. If you look at CoreWLAN in Catalina you will not see any calls to blockBSSIDAccess in scanForNetworksWithChannels, yet BSSIDs are still blocked. This functionality now happens in locationServicesBlockBSSIDAccessForXPCConnection.

Except this doesn’t get called from anywhere in CoreWLAN so we need to go looking elsewhere. nming through many Apple libraries and binaries I found that function is called by /usr/libexec/airportd. It’s unclear why they kept this function in CoreWLAN however. It’s possible something I didn’t find also calls it, or will call it (in a future version of macOS Catalina) because I would expect this to have been moved to airportd.

But now we understand the flow and this call happens during the XPC call, never giving us the BSSIDs to steal (fixing the structural problem). Except here’s the relevant part of the disassembly:

So when does this return false? When:

  • You have the entitlement: com.apple.wifi.bypass-location-services OR

  • You have the entitlement: com.apple.wifi.priority.internal OR

  • Your last path component is WirelessStress OR

  • Location Services are enabled AND

    • Your last component path is SystemUIServer OR

  • locationServicesAuthorizedForPID returns true for your PID

So renaming your binary to WirelessStress bypasses this new check, allowing you to retrieve BSSIDs like you could in High Sierra, or Mojave with the previous exploit.

Conclusion

Apple fixed the harder of the two bugs in Catalina, the code is now structured correctly and only a single check is bad. I’m not sure how they knew to remove the package check but the last path component one was left in. Perhaps they thought that binaries cannot be named in collision with Apple binaries? More likely maybe they didn’t have time to give WirelessStress the correct entitlements before launch. If that’s the case, we should see this fixed before the end of the Catalina beta period so time will tell. Most likely I think they only fixed the exact thing I reported (without looking for any similar issues) and I never mentioned WirelessStress in my bug report.

The good news is that this is a very easy fix for Apple now, removing that one check should fully patch this vulnerability and then I’ll have to look for something else.

Edit: As of Catalina Beta 7 this version of the bug is fixed so will no longer be exploitable when Catalina launches.