导语:本文将讨论我们在macOS上评估Safari沙盒的安全性漏洞的经验。我们将选择一个暴露在沙盒中的软件组件,并使用Frida构建一个进程内fuzzer,并将其作为发现可利用漏洞的一个手段。
当开发实体软件或设备时,实现系统上的任意代码执行可能仅仅是迈向完全攻击的第一步。对于具有高价值的或安全意识比较强的目标,远程代码执行通常通过沙盒逃逸(或特权升级)及其持久性来完成。每一阶段通常都需要其自身完整实现独特的攻击,使一些武器化的zero-day(零日)成为一系列攻击的“链”。
考虑到高风险的消费者软件,现代web浏览器使用软件沙盒来缓解远程攻击事件中的损害。在上一篇文章中利用了Apple Safari之后,本文将注意力转向macOS上实现对系统全面攻击的Safari沙盒逃逸。
使用Frida对锁屏上的macOS WindowServer进行模糊测试
作为Pwn2Own系列的第五篇博文,本文将讨论我们在macOS上评估Safari沙盒的安全性漏洞的经验。我们将选择一个暴露在沙盒中的软件组件,并使用Frida构建一个进程内fuzzer,并将其作为发现可利用漏洞的一个手段。
软件沙盒
通过操作系统提供的依赖于平台的安全特性来限制应用程序的运行时特权,通常可以实现软件沙盒。当适当的分层时,这些安全控件可以限制应用程序与更广泛的系统(syscall过滤、服务ACL)通信的能力,阻止它在磁盘上读取/写入文件,并阻塞外部资源(网络)。
针对特定的应用程序,沙盒将有效的减少系统暴露于潜在恶意进程,并防止进程对机器进行持久的更改。例如,如果进程不允许文件系统访问,则一个被攻击的但受到沙盒保护的应用程序,不能在磁盘上偿还用户文件。
大约是2010年的一个旧的Adobe Reader保护模式沙盒图表
在过去的几年中,我们已经看到沙盒在隔离有问题的软件方面变得更加安全。这引发了关于理论上完美软件沙盒价值的讨论:如果已经适当的控制了这些沙盒,那么攻击者是否能够在机器上获得任意代码执行的权利,真的还重要吗?
这个问题的答案在安全研究人员中引起了激烈的争论。微软Edge和谷歌Chrome进行的浏览器安全性的对比,进一步加剧了这一争论。其中一种方法主张进程内利用缓解措施(Edge),另一种是隔离技术(Chrome)的典型代表。
作为一个简单的晴雨表,Pwn2Own在过去几年中得出的结果似乎表明,与先进的进程内缓解措施相比,沙盒更胜一筹。关于为什么会出现这种情况,以及这种趋势在现实世界中是否真的存在,有无数的观点。
简单地说,作为攻击者,我们确实认为沙盒(如果做对了)增加了相当大的价值来保护软件。更重要的是,这也是许多熟悉攻击这些产品的人的观点。
THE (MEMORY CORRUPTION) SAFETY DANCE (13:25)在SAS 2017,Mark Dowd
然而,随着技术的改进以及缓解措施的取代,例如,严格控制流完整性 (CFI),这些观点可能会改变。最近由“Meltdown & Spectre”所带来的启示就是一个很好的例子,它甚至将裂缝引入了理论上完美的沙盒。
最后,沙盒和缓解技术都将继续改进和发展。它们不是相互排斥的,而是以不同的方式在提高开发成本方面发挥着重要作用。
macOS沙盒文件
在macOS上,有一种强大的低级沙盒技术叫做“安全带”(Seatbelt),苹果公司(公开)已经弃用这种技术,而支持更高级的“应用沙盒”(App Sandbox)。由于可用的官方文档很少,关于如何使用以前的系统沙盒的信息已经通过社区逆向工程了解(1,2,3,4,5,…)。
简而言之,以Seatbelt为基础的macOS沙盒壁垒,是用在人类可读的沙盒配置文件中定义的规则构建的。这些沙盒配置文件中有一些在磁盘上,可以根据特定应用程序的特定需求进行定制。
对于Safari浏览器,其沙盒配置文件由以下文件组成(位置可能不同):
/System/Library/Sandbox/Profiles/system.sb /System/Library/StagedFrameworks/Safari/WebKit.framework/Versions/A/Resources/com.apple.WebProcess.sb
macOS沙盒配置文件是用一种叫做TinyScheme的语言编写的。配置文件通常被编写为应用程序所需操作或服务的白名单,默认情况下不允许访问更广泛的系统。
... (version 1) (deny default (with partial-symbolication)) (allow system-audit file-read-metadata) (import "system.sb") ;;; process-info* defaults to allow; deny it and then allow operations we actually need. (deny process-info*) (allow process-info-pidinfo) ...
例如,沙盒配置文件可以将显式目录或文件加入白名单,并且应该允许沙盒应用程序访问这些目录或文件。这是WebProceess.sb配置文件的一个片段,允许Safari只读访问在磁盘上存储用户首选项的某些目录:
... ;; Read-only preferences and data (allow file-read* ;; Basic system paths (subpath "/Library/Dictionaries") (subpath "/Library/Fonts") (subpath "/Library/Frameworks") (subpath "/Library/Managed Preferences") (subpath "/Library/Speech/Synthesizers") ...
沙盒配置文件提供的服务几乎与horse blinder一样,通过列出我们可以与哪些非沙盒资源进行交互,沙盒配置文件帮助我们将注意力集中(作为攻击者)在系统上。这有助于枚举相关的攻击面,探测这些攻击面可以发现安全缺陷。
沙盒逃逸
在实践中,沙盒逃逸通常是它们自己的独立利用。这意味着一个用于逃避浏览器沙盒的利用程序与用于实现初始远程代码执行的利用程序几乎总是完全不同的。
当逃逸软件沙盒时,利用程序通常会攻击在沙盒进程之外执行的代码。通过利用运行在沙盒之外的内核或应用程序(例如系统服务),熟练的攻击者可以将自己的程序转向没有沙盒的执行上下文。
Safari沙盒策略显式地白名单化了许多外部软件攻击面。例如,下面的策略片段突出显示了许多IOKit接口,这些接口可以从沙盒中访问。这是因为它们公开了浏览器中某些特性所需的系统控件。
... ;; IOKit user clients (allow iokit-open (iokit-user-client-class "AppleMultitouchDeviceUserClient") (iokit-user-client-class "AppleUpstreamUserClient") (iokit-user-client-class "IOHIDParamUserClient") (iokit-user-client-class "RootDomainUserClient") (iokit-user-client-class "IOAudioControlUserClient") ...
在整个配置文件中,以iokit-*开头的条目引用可以通过IOKit框架调用的功能。这些是用户客户端(接口),可以用来与相关的内核对等物(kexts)进行通信。
沙盒配置文件中定义的另一种有趣的规则类型属于allow mach-lookup:
... ;; Remote Web Inspector (allow mach-lookup (global-name "com.apple.webinspector")) ;; Various services required by AppKit and other frameworks (allow mach-lookup (global-name "com.apple.FileCoordination") (global-name "com.apple.FontObjectsServer") (global-name "com.apple.PowerManagement.control") (global-name "com.apple.SystemConfiguration.configd") (global-name "com.apple.SystemConfiguration.PPPController") (global-name "com.apple.audio.SystemSoundServer-OSX") (global-name "com.apple.analyticsd") (global-name "com.apple.audio.audiohald") ...
上面描述的allow mach-lookup关键字被用于允许沙盒应用程序访问系统服务中驻留的各种远程过程调用(RPC)类服务器。这些策略定义允许应用程序在mach IPC上与这些白名单化的RPC服务器通信。
此外,还有一些显式白名单化的XPC服务:
... (deny mach-lookup (xpc-service-name-prefix "")) (allow mach-lookup (xpc-service-name "com.apple.accessibility.mediaaccessibilityd") (xpc-service-name "com.apple.audio.SandboxHelper") (xpc-service-name "com.apple.coremedia.videodecoder") (xpc-service-name "com.apple.coremedia.videoencoder") ...
XPC是更高级别的IPC,用于促进进程之间的通信,同样构建在mach IPC的顶部。XPC具有很好的文档记录,网上有大量的可用资源和安全研究(1,2,3,4,…)。
还有其他一些有趣的途径可以攻击非沙盒代码,包括直接将syscall发送到XNU内核,或者通过IOCTL。由于时间原因,我们不对其进行探讨。
我们对沙盒的评估很简洁。但是未来,一个更有趣的练习是枚举当前不能被沙盒策略约束的攻击面。
目标选择
在检查了暴露在Safari沙盒中的一些组件之后,下一步是确定我们认为最容易成为目标的一种逃逸方法。
攻击位于macOS内核中的组件是很有吸引力的:成功的利用不仅保证了沙盒逃逸,还保证了无限制的ring-zero代码执行。随着macOS 10.11 (El Capitan)中“rootless”的引入,内核模式特权升级对于诸如在不禁用SIP的情况下加载未签名驱动程序之类的情况是必要的。
攻击内核代码的缺点是以可调试性和便利性为代价的。用于调试或测试内核代码的工具比较原始并且没有记录,或者基本上不存在。出现复制错误、分析崩溃或稳定漏洞通常需要完整的系统重新启动,这可能会增加时间和并影响士气。
在权衡了这些特性并回顾了过去Safari沙盒的公共研究之后,我们将注意力集中在了WindowServer上。一个复杂的用户模式系统服务,可以通过mach IPC访问Safari沙盒:
(allow mach-lookup ... (global-name "com.apple.windowserver.active") ... )
就我们的目的而言,WindowServer几乎是一个理想的目标:
· 几乎每个进程都可以与之通信(包括Safari)
· 它位于userland中,简化了调试和自省
· 它运行时的权限基本上等同于root
· 它有一个相对较大的攻击面
· 它有一个值得注意的安全漏洞的历史
WindowServer是一个封闭的、私有的框架(一个库),它意味着开发人员不打算直接与之交互。这也意味着官方文件是不存在的,公开的信息很少、要么是过时的,或者是不完整的。
WindowServer攻击面
WindowServer通过处理来自系统上运行应用程序的传入mach_message来工作。在macOS上,mach_messages是IPC的一种形式,使得正在运行的进程之间可以进行通信。系统服务通常使用Mach IPC来公开一个RPC接口,供其他应用程序调用。
在系统内部,几乎每个GUI macOS应用程序都透明的与windows服务器通信。顾名思义,WindowServer系统服务负责将应用程序窗口绘制到屏幕上。一个正在运行的应用程序将提供给WindowServer(通过RPC)其将要创建窗口的大小或形状,以及放置窗口的位置:
在macOS上,WindowServer几乎呈现所有桌面应用程序
对于那些熟悉微软Windows操作系统的人来说,macOS WindowServer有点像用户模式Win32k,尽管不那么复杂。它还负责绘制鼠标光标、管理热键和促进一些跨进程通信(以及其他许多事情)。
应用程序可以在mach IPC上与windows服务器交互,以达到大约600个类似RPC的功能。当拥有特权的WindowServer系统服务接收到mach_message时,将被路由到其各自的消息处理程序(一个“远程程序”),并与处理程序函数解析的外部数据相结合。
WindowServer mach消息处理程序的选择
作为攻击者,这些以_X…为前缀的函数(如_XBindSurface)表示可直接访问的攻击面。从Safari沙盒中,我们可以向WindowServer发送任意的mach消息(数据),目标是这些函数中的任何一个。如果我们能够在其中一个函数中找到漏洞,我们就可以利用这个服务。
我们发现,在WindowServer中,这600个处理程序函数在MIG生成的三个mach子系统中被分割。每个子系统都有自己的消息调度程序,它首先解析传入的mach消息的头,然后通过间接调用将消息特定的数据传递给相应的处理器:
RAX是消息处理程序函数的代码指针,该函数根据传入的消息id进行选择
这三个分派子系统是使用动态二进制插桩(DBI)促进理想位置模糊进程内的WindowServer。在传入数据被传递到约600个单独消息处理程序的任意一个之前,这三个子系统代表了的普通“last hop”。
不需要逆向工程任何这些表层函数或它们的唯一消息格式(输入),我们已经发现了一个低成本的方法来开始自动发现漏洞。通过检测这些阻塞点,我们可以模糊所有通过正常用户与系统的交互生成的WindowServer传入流量。
使用Frida进程内模糊测试
Frida是一个DBI框架,它向目标进程注入一个JavaScript解释器,通过用户提供的脚本启用黑盒检测。这听起来像是JavaScript的奇怪用法,但是这个模型允许对编译后的应用程序进行几乎是无限的二进制内省的快速原型化。
通过为指令定义一个小表,我们开始编写Frida fuzzing脚本,我们希望在运行时可以hook这些指令。这些指令中的每一条都是在前一节中涉及的分派例程中的间接调用(例如,call rax)。
// instructions to hook (offset from base, reg w/ call target)var targets = [ ['0x1B5CA2', 'rax'], // WindowServer_subsystem ['0x2C58B', 'rcx'], // Renezvous_subsystem ['0x1B8103', 'rax'] // Services_subsystem]
Frida提供的JavaScript API有各种允许用户窥探或修改流程运行时的功能。使用Interceptor API,可以hook单个指令作为停止和检查进程的地方。hook代码的基础如下:
function InstallProbe(probe_address, target_register) { var probe = Interceptor.attach(probe_address, function(args) { var input_msg = args[0]; // rdi (the incoming mach_msg) var output_msg = args[1]; // rsi (the response mach_msg) // extract the call target & its symbol name (_X...) var call_target = this.context[target_register]; var call_target_name = DebugSymbol.fromAddress(call_target); // ready to read / modify / replay console.log('[+] Message received for ' + call_target_name); // ... }); return probe;}
为了hook前面定义的指令,我们首先解析了指令所在的私有SkyLight框架的基本地址。然后,我们可以使用模块base + offset在运行时计算目标指令的虚拟地址。之后,就像在这些地址上安装拦截器一样简单:
// locate the runtime address of the SkyLight frameworkvar skylight = Module.findBaseAddress('SkyLight');console.log('[*] SkyLight @ ' + skylight);// hook the target instructionsfor (var i in targets) { var hook_address = ptr(skylight).add(targets[i][0]); // base + offset InstallProbe(hook_address, targets[i][1]) console.log('[+] Hooked dispatch @ ' + hook_address);}
在已安装的消息拦截期间,我们现在可以在mach消息内容被传递到底层消息处理程序之前对其进行记录、修改或重放(an _X…函数)。这使我们能够有效的中间人攻击任何到这些MIG子系统的mach流量,并在运行时转储MIG子系统的内容:
使用Frida来嗅探WindowServer接收到的传入mach消息
从这一点来看,我们的模糊测试策略很简单。我们使用hook对WindowServer接收的任何传入消息上的随机二进制数据进行翻转(非自动化模糊测试)。同时,我们记录了由我们的模糊器注入的bitflips,以创建“replay”日志文件。
在WindowServer的一个新实例中replay记录的bitflips,可以为我们的模糊器产生的任何崩溃提供一定程度的可再现性。在尝试识别底层bug时,持续重现崩溃的能力是无价的。bitflip重播日志的示例片段如下:
... {"msgh_bits":"0x1100","msgh_id":"0x7235","buffer":"000000001100000001f65342","flip_offset":[4],"flip_mask":[16]} {"msgh_bits":"0x1100","msgh_id":"0x723b","buffer":"00000000010000000900000038a1b63e00000000"} {"msgh_bits":"0x80001112","msgh_id":"0x732f","buffer":"0000008002000000ffffff7f","ool_bits":"0x1000101","desc_count":1} {"msgh_bits":"0x1100","msgh_id":"0x723b","buffer":"00000000010000000900000070f3a53e00000000","flip_offset":[12],"flip_mask":[2]} {"msgh_bits":"0x80001100","msgh_id":"0x722a","buffer":"0000008002000000dfffff7f","ool_bits":"0x1000101","desc_count":1,"flip_offset":[8],"flip_mask":[32]} ...
为了使模糊器有效,最后一步需要我们刺激系统生成WindowServer消息的“通信”。这可以通过多种方式实现,比如让用户在系统中导航,或者编写脚本随机打开应用程序并移动它们。
但是,通过对流行文化和过去漏洞的仔细研究,我们决定简单地在“Enter”键上放置一个权重:
高级的持续威胁
在macOS lockscreen上,按住“Enter”碰巧会为WindowServer生成各种各样的合理的消息流量。当由于bitflip而发生崩溃时,我们将重播日志和崩溃状态保存到磁盘。
幸运的是,当WindowServer崩溃时,macOS锁住了机器,重新启动了服务,并返回锁屏。在后台运行的一个简单的python脚本会弹出新的WindowServer实例,注入Frida以启动下一轮的模糊检测(fuzzing)。
这是我们为实现这一目标所能做的最少的努力以及付出的最低的成本,但它仍然是卓有成效的。
发现和根本原因分析
让模糊器在夜间运行,它产生了许多独特的(大部分是无用的)崩溃。在为数不多的几次更有趣的崩溃中,有一次看起来特别有希望,但需要进一步调查。
我们对附加了lldb(默认的macOS调试器)的WindowServer新实例replay了那次崩溃的bitflip日志,并重现这个问题。崩溃指令和寄存器状态描述了像是Out-of-Bounds Read:
Process 77180 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (address=0x7fd68940f7d8) frame #0: 0x00007fff55c6f677 SkyLight`_CGXRegisterForKey + 214 SkyLight`_CGXRegisterForKey: -> 0x7fff55c6f677 <+214>: mov rax, qword ptr [rcx + 8*r13 + 0x8] 0x7fff55c6f67c <+219>: test rax, rax 0x7fff55c6f67f <+222>: je 0x7fff55c6f6e9 ; <+328> 0x7fff55c6f681 <+224>: xor ecx, ecx Target 0: (WindowServer) stopped.
在崩溃的上下文中,r13看起来完全无效(非常大)。
这次崩溃的另一个吸引人的因素是它接近顶级的\_X…函数。这次崩溃的浅层本质意味着我们可能会直接控制导致这次事故的错误字段。
(lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (address=0x7fd68940f7d8) * frame #0: 0x00007fff55c6f677 SkyLight`_CGXRegisterForKey + 214 frame #1: 0x00007fff55c28fae SkyLight`_XRegisterForKey + 40 frame #2: 0x00007ffee2577232 frame #3: 0x00007fff55df7a57 SkyLight`CGXHandleMessage + 107 frame #4: 0x00007fff55da43bf SkyLight`connectionHandler + 212 frame #5: 0x00007fff55e37f21 SkyLight`post_port_data + 235 frame #6: 0x00007fff55e37bfd SkyLight`run_one_server_pass + 949 frame #7: 0x00007fff55e377d3 SkyLight`CGXRunOneServicesPass + 460 frame #8: 0x00007fff55e382b9 SkyLight`SLXServer + 832 frame #9: 0x0000000109682dde WindowServer`_mh_execute_header + 3550 frame #10: 0x00007fff5bc38115 libdyld.dylib`start + 1 frame #11: 0x00007fff5bc38115 libdyld.dylib`start + 1
找出导致这次崩溃的错误的根本原因分析只用了几分钟。在崩溃之前,_CGXRegisterForKey(…)中有一个签名/未签名的比较问题:
WindowServer中签名的比较漏洞
WindowServer尝试确保用户控制的索引参数为6或更少。但是,这个检查是作为一个签名整数比较实现的。这意味着提供一个任意大小的负数(如,-100000)将错误的使我们通过检查。
“fuzzed”索引是_XRegisterForKey(…)的mach消息中的一个32位字段。fuzzer翻转的碰巧是最上面的bit,把数字变成了一个巨大的负值:
HEX | BINARY | DECIMAL ----------+----------------------------------+------------- BEFORE: 0x0000005 | 00000000000000000000000000000101 | 5 AFTER: 0x8000005 | 10000000000000000000000000000101 | -2147483643 ^ |- Corrupted bit
假设我们可以通过对有效内存进行仔细的索引来成功读取当前的崩溃,那么在我们和之后函数中看起来像是可利用的写入之间就有一些小的限制:
攻击者控制的越界(Out-of-Bounds)索引上可以写入未知值(r15, ecx)
在正确的条件下,这个bug看起来是Out-of-Bounds Write!任何允许内存损坏(一个写入)的漏洞通常被归类为可利用的条件(除非证明不是这样)。这个漏洞后来被修正为CVE-2018-4193。
总结
在利用现代浏览器时,软件沙盒逃逸是实现对整个系统攻击的必要步骤。本文讨论了沙盒技术的价值、沙盒逃逸的标准方法,以及评估Safari沙盒以实现沙盒逃逸。
通过查看现有资源,我们设计了一种策略来处理Safari沙盒,并使用非常简单的进程内模糊器模糊历来存在问题的组件(WindowServer)。本文没有展示任何新奇的东西,即使是经过设计的模糊器也仍然能够找到关键的、现实世界的bug。