CVE-2021-1810:如何绕过Gatekeeper

robots

 

0x00 前言

当我们在macOS上使用Archive Utility来解压时,如果文件路径长度超过886个字符,就无法继承com.apple.quarantine扩展属性,导致这类文件有可能绕过Gatekeeper。因此,即使macOS的Gatekeeper强制启用了代码签名,有针对性的攻击者也有可能在该系统上执行未签名的程序。

该漏洞已经在macOS Big Sur 11.3以及Security Update 2021-002 Catalina中修复。

 

0x01 漏洞分析

当我们在Finder中双击一个归档文件时,系统会调用Archive Utility来处理文件,而Archive Utility会将实际的解压流程交给ArchiveService进程来处理。我启动了一个内置的DTrace脚本(newproc.d),然后在Finder中打开一个zip文件,此时会有如下进程启动:

2021 Jan 12 15:23:50 71676 <1> 64b  xpcproxy com.apple.xpc.launchd.oneshot.0x10000083.Archive Utility
2021 Jan 12 15:23:51 71677 <1> 64b  xpcproxy com.apple.XprotectFramework.AnalysisService 71143
2021 Jan 12 15:23:51 71677 <1> 64b  /System/Library/PrivateFrameworks/XprotectFramework.framework/Versions/A/XPCServices/XprotectService.xpc/Contents/MacOS/Xprotect (...)
2021 Jan 12 15:23:51 71676 <1> 64b  /System/Library/CoreServices/Applications/Archive Utility.app/Contents/MacOS/Archive Utility -psn_0_5776770
2021 Jan 12 15:23:51 71678 <71676> 64b  /usr/bin/macbinary probe --verbose /Users/user/Downloads/archive-8.zip
2021 Jan 12 15:23:51 71679 <71676> 64b  /usr/bin/file -b /Users/user/Downloads/archive-8.zip
2021 Jan 12 15:23:51 71680 <1> 64b  xpcproxy com.apple.archiveutility.auhelperservice 71676
2021 Jan 12 15:23:51 71680 <1> 64b  /System/Library/CoreServices/Applications/Archive Utility.app/Contents/XPCServices/AUHelperService.xpc/Contents/MacOS/AUHelperSe (...)
2021 Jan 12 15:23:52 71681 <1> 64b  xpcproxy com.apple.FileProvider.ArchiveService 71676
2021 Jan 12 15:23:52 71681 <1> 64b  /System/Library/Frameworks/FileProvider.framework/XPCServices/ArchiveService.xpc/Contents/MacOS/ArchiveService
2021 Jan 12 15:23:52 71682 <1> 64b  xpcproxy com.apple.appkit.xpc.sandboxedServiceRunner 71676
2021 Jan 12 15:23:52 71682 <1> 64b  /System/Library/Frameworks/AppKit.framework/Versions/C/XPCServices/SandboxedServiceRunner.xpc/Contents/MacOS/SandboxedServiceRun (...)

排除掉xpcproxy引导进程、XProtect以及文件类型判定进程(比如macprobefile)后,这里还涉及到3个关键的进程:

  • Archive Utility
  • AUHelperService
  • ArchiveService

配合fs_usage进行分析后,很明显可以发现,zip写文件的过程由ArchiveService负责完成。此外,根据fs_usageArchiveService会将zip文件中提取的所有文件写入一个临时目录,比如:

/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)

这是[NSFileManager URLForDirectory:inDomain:appropriateForURL:create:error]返回的一个标准临时目录,对于这个测试场景,最终会在Catalina系统中生成长度为152字符的一个路径。

这样也决定了解压文件的绝对路径的长度。在之前的测试中我们可以看到,当完整路径(临时路径作为前缀,拼接上zip文件中提取的路径)超过1024个字节时,就会导致com.apple.quarantine属性丢失。在Big Sur上,路径长度为137个字符:

/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/NSIRD_ArchiveService_w86Nam

这里补充一下,在10.14上,Archive Utility(不是ArchiveService)提取生成的路径为:

/private/var/folders/tq/06xccl452c735h1sfkqb85_r0000gn/T/Cleanup At Startup/.BAH.qJNqN

这里包含86个字符。然而,10.14似乎并不存在这个bug。如果任何路径长度超过了PATH_MAX,那么Archive Utility会直接停止工作,抛出“文件名过长”的一个消息。当时苹果可能认为这是一个bug。当这个bug被修复后,漏洞自然就会浮出水面。

这里我们很明显要考虑到操作系统的具体版本。比如,在Catalina以及Big Sur上利用这个漏洞时,需要考虑到不同的路径长度。

此时我们有了一个猜想,可以通过一些简单的实验进行测试:

1、ArchiveService创建一个临时目录;

2、由于zip文件拥有com.apple.quarantine属性,因此当ArchiveService将提取的文件写入临时目录时,会在每个文件上设置这个属性;

3、当提取出的临时文件的完整路径长度超过PATH_MAX时,将无法设置quarantine属性;

4、ArchiveService似乎并不在意这个细节,会继续解压,导致提取出的文件缺少quarantine属性。

后来我发现这个猜想并不完全正确,但还是可以指导我们找到真正的bug,以及如何进行修复。

由于临时文件所使用的路径前缀比典型的目标目录(/Users/username/Downloads/subfolder)更长,因此当这些文件被移动到目标地址时,有些路径名长度在临时目录中会超过PATH_MAX,但在最终的目标文件夹中会比PATH_MAX更短(因此可以执行)。

如果我们想部署一个看上去人畜无害的应用时,我们需要考虑标准的macOS目录结构以及应用名。比如,如果我们的zip文件中需要名为FakeApp.app的一个标准macOS应用,那么需要满足如下条件(以Catalina为例):

1、152字节用于临时目录路径名称;

2、11字节用于FakeApp.app目录;

3、1024 – 11 – 152 = 861字节用于“填充”。

每个目录名只能为255个字节长,因此861/255 = 3,余数为96。

在如下示例中,我在macOS 10.15.7 build 19H512进行了相应的测试,这主要是因为我无法在VMWare的Big Sur中禁用SIP,出了一些问题。除此之外,Big Sur本身的工作流程与我测试的系统大体相同。

ArchiveService进程开始解压我提供的zip poc文件时,我使用Dtruss进行分析。由于ArchiveScanner是按需启动的一个作业任务,我使用的命令如下:

dtruss -s -W ArchiveService

这样当双击zip文件,启动ArchiveScanner进程时,我们就可以成功attach。

通过这种方式,我们的确获取了一些有趣的信息,比如这里涉及到setattrlistat()系统调用,并且调用会返回ENAMETOOLONG错误值:

setattrlistat(0xFFFFFFFFFFFFFFFE, 0x7F91B085E600, 0x700005F24530)        = -1 Err#63

              libsystem_kernel.dylib`setattrlistat+0xa
              ...
              ArchiveService`0x0000000106d0d055+0x454
              Foundation`-[NSFileCoordinator _invokeAccessor:thenCompletionHandler:]+0x8f
              ...

为了得到关键证据来验证猜想,我需要attach一个调试器,在setattrlist()设置一个断点。由于这也是一个按需运行的XPC服务,我们需要等待服务启动。有一种方法可以完成该任务:lldb --wait-for --attach-name ArchiveService,然后在setattrlistat处设置断点:

rasmus@catalina-beta-vm ~ % lldb --wait-for --attach-name ArchiveService
(lldb) process attach --name "ArchiveService" --waitfor
Process 1359 stopped
* thread #3, stop reason = signal SIGSTOP
    frame #0: 0x00007fff727da502 libsystem_kernel.dylib`__sigsuspend_nocancel + 10
libsystem_kernel.dylib`__sigsuspend_nocancel:
->  0x7fff727da502 <+10>: jae    0x7fff727da50c            ; <+20>
    0x7fff727da504 <+12>: movq   %rax, %rdi
    0x7fff727da507 <+15>: jmp    0x7fff727d5629            ; cerror_nocancel
    0x7fff727da50c <+20>: retq   
Target 0: (ArchiveService) stopped.

Executable module set to "/System/Library/Frameworks/FileProvider.framework/XPCServices/ArchiveService.xpc/Contents/MacOS/ArchiveService".
Architecture set to: x86_64h-apple-macosx-.
(lldb) break set -n setattrlistat
Breakpoint 1: where = libsystem_kernel.dylib`setattrlistat, address = 0x00007fff727f5b5c
(lldb)

与此同时,在另一个Terminal窗口中,我们可以启动一个fs_usage实例来跟踪ArchiveService的文件系统行为。这样我们就可以很方便地识别ArchiveService所使用的临时目录,并且直到ArchiveService继续运行、命中下一次断点前,我们可以监控目录更改情况:

Process 1412 resuming
Process 1412 stopped
* thread #4, queue = 'NSOperationQueue 0x7fdcc640fbe0 (QOS: UNSPECIFIED)', stop reason = breakpoint 1.1
    frame #0: 0x00007fff727f5b5c libsystem_kernel.dylib`setattrlistat
libsystem_kernel.dylib`setattrlistat:
->  0x7fff727f5b5c <+0>:  movl   $0x200020c, %eax          ; imm = 0x200020C 
    0x7fff727f5b61 <+5>:  movq   %rcx, %r10
    0x7fff727f5b64 <+8>:  syscall 
    0x7fff727f5b66 <+10>: jae    0x7fff727f5b70            ; <+20>
Target 0: (ArchiveService) stopped.

根据上述信息,我们发现setattrlistat()与扩展属性没有任何关系。实际上,当ArchiveService进程退出时,临时目录中并没有quarantine属性。我也使用相同的方法调查了AUHelperService,发现并没有与设置扩展属性相关的任何活动。

为了确定哪个函数负责扩展属性,我一开始写了个小程序,使用Apple的EndpointSecurity API,监听ES_EVENT_TYPE_NOTIFY_SETEXTATTR事件,以便监控所有属性。结果表明,EndpointSecurity并没有对外暴露与com.apple.quarantine扩展属性有关的事件,这可能是为了避免(有意或无意的)引入Gatekeeper绕过漏洞。

由于需要另一种不同的办法,我列出了提到xattr的所有Dtrace探针,更具体一点,关注的是setxattr()

dtrace -l | grep xattr命令列出来199个探针,过滤setxattr后,探针数量减少到了33个。

root@catalina-beta-vm ~ # dtrace -l | grep setxattr   
  630    syscall                                            setxattr entry
  631    syscall                                            setxattr return
  632    syscall                                           fsetxattr entry
  633    syscall                                           fsetxattr return
 2201     fsinfo       mach_kernel                     VNOP_SETXATTR setxattr
105788        fbt com.apple.filesystems.apfs                apfs_vnop_setxattr entry
105789        fbt com.apple.filesystems.apfs                apfs_vnop_setxattr return
105830        fbt com.apple.filesystems.apfs      apfs_setxattr_as_namedstream entry
105831        fbt com.apple.filesystems.apfs      apfs_setxattr_as_namedstream return
105832        fbt com.apple.filesystems.apfs            apfs_setxattr_internal entry
105833        fbt com.apple.filesystems.apfs            apfs_setxattr_internal return
106923        fbt com.apple.filesystems.apfs           apfs_fake_vnop_setxattr entry
106924        fbt com.apple.filesystems.apfs           apfs_fake_vnop_setxattr return
120856        fbt com.apple.filesystems.hfs.kext         hfs_exchangedata_setxattr entry
120857        fbt com.apple.filesystems.hfs.kext         hfs_exchangedata_setxattr return
121056        fbt com.apple.filesystems.hfs.kext                 hfs_vnop_setxattr entry
121057        fbt com.apple.filesystems.hfs.kext                 hfs_vnop_setxattr return
121058        fbt com.apple.filesystems.hfs.kext             hfs_setxattr_internal entry
121059        fbt com.apple.filesystems.hfs.kext             hfs_setxattr_internal return
165623        fbt       mach_kernel                nfs4_vnop_setxattr entry
165624        fbt       mach_kernel                nfs4_vnop_setxattr return
166451        fbt       mach_kernel               fpnfs_vnop_setxattr entry
166452        fbt       mach_kernel               fpnfs_vnop_setxattr return
167900        fbt       mach_kernel                          setxattr entry
167901        fbt       mach_kernel                          setxattr return
167902        fbt       mach_kernel                         fsetxattr entry
167903        fbt       mach_kernel                         fsetxattr return
168122        fbt       mach_kernel                       vn_setxattr entry
168123        fbt       mach_kernel                       vn_setxattr return
187600        fbt       mach_kernel                 mac_vnop_setxattr entry
187601        fbt       mach_kernel                 mac_vnop_setxattr return
187828        fbt       mach_kernel                 mac_file_setxattr entry
187829        fbt       mach_kernel                 mac_file_setxattr return

我也使用了一个DTrace脚本,来匹配以上所有探针,打印出堆栈信息(内核及用户空间),如下所示:

::*setxattr*:entry { 
    ustack();
    stack();
}

这里我们从Archive Utility进程找到了一些有趣的调用:

1 105832     apfs_setxattr_internal:entry 
  libsystem_kernel.dylib`__mac_syscall+0xa
  libquarantine.dylib`_qtn_file_apply_to_path+0x68
  Archive Utility`0x0000000101c1988f+0xc2
  Archive Utility`0x0000000101c181ef+0x244
  Foundation`__NSThread__start__+0x428
  libsystem_pthread.dylib`_pthread_start+0x94
  libsystem_pthread.dylib`thread_start+0xf

  apfs`apfs_vnop_setxattr+0x189
  kernel`vn_setxattr+0x2b2
  kernel`mac_vnop_setxattr+0x105
  Quarantine`quarantine_set_ea+0x5d
  Quarantine`syscall_quarantine_setinfo_common+0x5e2
  Quarantine`syscall_quarantine_setinfo_path+0xd4
  kernel`__mac_syscall+0xee
  kernel`unix_syscall64+0x287
  kernel`hndl_unix_scall64+0x16

libquarantine.dylib中的_qtn_file_apply_to_path函数可能是一个潜在的目标。还有另一个有趣的信息:似乎有一个特殊的系统调用,专门用来设置com.apple.quarantine属性,该调用由Quarantine内核扩展来实现。现在我们关注的是用户空间,我决定在__qtn_syscall_quarantine_setinfo_path上设置一个断点。

(lldb) break set -n _qtn_file_apply_to_path
Breakpoint 1: where = libquarantine.dylib`_qtn_file_apply_to_path, address = 0x00007fff726c613b
(lldb) cont
Process 27579 resuming
Process 27579 stopped
* thread #8, stop reason = breakpoint 1.1
    frame #0: 0x00007fff726c613b libquarantine.dylib`_qtn_file_apply_to_path
libquarantine.dylib`_qtn_file_apply_to_path:
->  0x7fff726c613b <+0>: pushq  %rbp
    0x7fff726c613c <+1>: movq   %rsp, %rbp
    0x7fff726c613f <+4>: pushq  %r14
    0x7fff726c6141 <+6>: pushq  %rbx
Target 0: (Archive Utility) stopped.
(lldb) break set -a 0x7fff726c619e
Breakpoint 2: where = libquarantine.dylib`_qtn_file_apply_to_path + 99, address = 0x00007fff726c619e
(lldb) cont
Process 27579 resuming
Process 27579 stopped
* thread #8, stop reason = breakpoint 2.1
    frame #0: 0x00007fff726c619e libquarantine.dylib`_qtn_file_apply_to_path + 99
libquarantine.dylib`_qtn_file_apply_to_path:
->  0x7fff726c619e <+99>:  callq  0x7fff726c63f7            ; __qtn_syscall_quarantine_setinfo_path
    0x7fff726c61a3 <+104>: testl  %eax, %eax
    0x7fff726c61a5 <+106>: je     0x7fff726c61c9            ; <+142>
    0x7fff726c61a7 <+108>: callq  0x7fff726c7bd6            ; symbol stub for: __error
Target 0: (Archive Utility) stopped.
(lldb) x -f s $rdi
0x7f9a3a904200: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)"
(lldb) x -f s $rsi
error: failed to read memory from 0x3c.
(lldb) x -f s $rdx
0x7f9a39c05c90: "q/0083;60bca5e1;Safari;ED038CA1-1FD3-4A6A-B3DD-EF64B565C027"

第一个参数(在rdi寄存器中)包含指向C字符串的一个指针,该字符串包含临时路径。第二个参数(在rsi中)值为0x3c60),为扩展属性内容的长度。第三个参数为另一个C字符串,内容看上去与我们在com.apple.quarantine扩展属性中看到的一致(多了个前缀q/,具体意义不明)。然后我在每次执行时都显示$rdi$rsi的内容:

(lldb) br com add 2.1
Enter your debugger command(s).  Type 'DONE' to end.
> x -f s $rdi  
> x -f s $rdx…
(lldb) contProcess 27579 resuming
(lldb)  x -f s $rdi 
0x7f9a3a904200: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

(lldb)  x -f s $rdx
0x7f9a39c05c90: "q/0083;60bca5e1;Safari;ED038CA1-1FD3-4A6A-B3DD-EF64B565C027"
Process 27579 stopped
* thread #8, stop reason = breakpoint 2.1
    frame #0: 0x00007fff726c619e libquarantine.dylib`_qtn_file_apply_to_path + 99

此时查看路径,没有看到quarantine属性:

root@catalina-beta-vm ~ # ls -ld@ "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
drwxr-xr-x  3 rasmus  staff  96 Jun  5 17:23 /private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

现在继续在调试器中cont,再次检查:

root@catalina-beta-vm ~ # ls -ld@ "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
drwxr-xr-x@ 3 rasmus  staff  96 Jun  5 17:23 /private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    com.apple.quarantine    57

这是设置quarantine属性的位置:

(lldb) cont
Process 27579 resuming
(lldb)  x -f s $rdi 
0x7f9a3a904200: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

(lldb)  x -f s $rdx
0x7f9a39c05c90: "q/0083;60bca5e1;Safari;ED038CA1-1FD3-4A6A-B3DD-EF64B565C027"

Process 27579 stopped
* thread #8, stop reason = breakpoint 2.1
    frame #0: 0x00007fff726c619e libquarantine.dylib`_qtn_file_apply_to_path + 99
libquarantine.dylib`_qtn_file_apply_to_path:
->  0x7fff726c619e <+99>:  callq  0x7fff726c63f7            ; __qtn_syscall_quarantine_setinfo_path
    0x7fff726c61a3 <+104>: testl  %eax, %eax
    0x7fff726c61a5 <+106>: je     0x7fff726c61c9            ; <+142>
    0x7fff726c61a7 <+108>: callq  0x7fff726c7bd6            ; symbol stub for: __error
Target 0: (Archive Utility) stopped.
(lldb) cont
Process 27579 resuming
(lldb)  x -f s $rdi 
0x7f9a3a904200: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

(lldb)  x -f s $rdx
0x7f9a39c05c90: "q/0083;60bca5e1;Safari;ED038CA1-1FD3-4A6A-B3DD-EF64B565C027"

Process 27579 stopped
* thread #8, stop reason = breakpoint 2.1
    frame #0: 0x00007fff726c619e libquarantine.dylib`_qtn_file_apply_to_path + 99
libquarantine.dylib`_qtn_file_apply_to_path:
->  0x7fff726c619e <+99>:  callq  0x7fff726c63f7            ; __qtn_syscall_quarantine_setinfo_path
    0x7fff726c61a3 <+104>: testl  %eax, %eax
    0x7fff726c61a5 <+106>: je     0x7fff726c61c9            ; <+142>
    0x7fff726c61a7 <+108>: callq  0x7fff726c7bd6            ; symbol stub for: __error
Target 0: (Archive Utility) stopped.

进程在退出时并没有尝试在提取出的所有内容上设置quarantine属性,至少这个函数没有执行该操作。根据以上信息,迭代器在遇到太长的文件名时似乎会过早退出,但并没有检查_qtn_file_apply_to_path的返回值。这并不影响我们对漏洞的利用,甚至有可能比我的利用方式更加简单,也许一个足够长的路径就可以触发漏洞。我并没有深入研究,欢迎大家探讨。

为了确定是谁调用了_qtn_file_apply_to_path,以便收集更多信息,我又重复了这个过程:

(lldb) bt
* thread #9, stop reason = breakpoint 1.1
  * frame #0: 0x00007fff726c613b libquarantine.dylib`_qtn_file_apply_to_path
    frame #1: 0x0000000101734951 Archive Utility`___lldb_unnamed_symbol314$$Archive Utility + 194
    frame #2: 0x0000000101733433 Archive Utility`___lldb_unnamed_symbol305$$Archive Utility + 580
    frame #3: 0x00007fff3ae1e7b2 Foundation`__NSThread__start__ + 1064
    frame #4: 0x00007fff72898109 libsystem_pthread.dylib`_pthread_start + 148
    frame #5: 0x00007fff72893b8b libsystem_pthread.dylib`thread_start + 15
(lldb) image list
[  0] 27E91CD7-37B0-3E51-B1A1-D79B6EC9A961 0x0000000101721000 /System/Library/CoreServices/Applications/Archive Utility.app/Contents/MacOS/Archive Utility

我使用Hopper来检查0x0000000101734951-0x0000000101721000 = 0x13951偏移处的值:

 loc_100013951:
0000000100013951         mov        rdi, rbx  ; CODE XREF=-[BAHDecompressor _propagateQuarantineInformation]+173, -[BAHDecompressor _propagateQuarantineInformation]+179

反编译后的代码为:

/* @class BAHDecompressor */
-(void)_propagateQuarantineInformation {
    rdi = self;
    r12 = *ivar_offset(_qtInfo);
    COND = *(rdi + r12) == 0x0;
    if (!COND) {
            r14 = rdi;
            rax = [rdi copyTarget];
            rax = [rax retain];
            rax = objc_retainAutorelease(rax);
            var_40 = [rax fileSystemRepresentation];
            *(&var_40 + 0x8) = 0x0;
            [rax release];
            rax = fts_open$INODE64(&var_40, 0x1c, 0x0);
            if (rax != 0x0) {
                    rbx = rax;
                    rax = fts_read$INODE64(rax, 0x1c, 0x0);
                    if (rax != 0x0) {
                            do {
                                    if (((*(int16_t *)(rax + 0x58) & 0xffff) <= 0xd) && (!COND)) {
                                            _qtn_file_apply_to_path(*(r14 + r12), *(rax + 0x28), 0x0);
                                    }
                                    rax = fts_read$INODE64(rbx);
                            } while (rax != 0x0);
                    }
                    fts_close$INODE64(rbx);
            }
    }
    if (**___stack_chk_guard != **___stack_chk_guard) {
            __stack_chk_fail();
    }
    return;
}

这个函数显然使用了fts(3)类的函数来遍历目录层次结构。重要的是,它使用了(变体版的)fts_read()函数,将函数结果传递给_qtn_file_apply_to_path()fts_read会返回一个FTSENT结构,如下所示:

typedef struct _ftsent {
struct _ftsent *fts_cycle;  /* cycle node (8 bytes) */
struct _ftsent *fts_parent; /* parent directory (8 bytes, total 16) */
struct _ftsent *fts_link;   /* next file in directory (8 bytes, total 24) */
long fts_number;            /* local numeric value (8 bytes, 32) */
void *fts_pointer;          /* local address value (8 bytes, 40) */
char *fts_accpath;      /* access path (8, 48)*/ 
char *fts_path;         /* root path (8, 56) */
int fts_errno;          /* errno for this node (4, 60) */
int fts_symfd;          /* fd for symlink (4, 64) */
unsigned short fts_pathlen; /* strlen(fts_path) (2, 66) */
unsigned short fts_namelen; /* strlen(fts_name) (2, 68) */
ino_t fts_ino;          /* inode (8, 76) */
dev_t fts_dev;          /* device (4, 80) */
nlink_t fts_nlink;      /* link count (2, 82) */
short fts_level;        /* depth (-1 to N) (2, 84) */
unsigned short fts_instr;   /* fts_set() instructions (2, 86) */
struct stat *fts_statp;     /* stat(2) information (8, 94) */
char fts_name[1];       /* file name */
} FTSENT;

第二个参数为这个结构体的0x28偏移值,对应的是上面的fts_accpath结构。查看一下这个值:

(lldb) x -f A $rax+40
0x6000018c0ca8: 0x00007f8fb813fa00
...
(lldb) x -f s 0x00007f8fb813fa00
0x7f8fb813fa00: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/FakeApp.app"

这个函数会将fts_accpath传递给_qtn_file_apply_to_path(),当这个路径大于1024字节时,代码逻辑会发生变化。为了验证完整路径超过PATH_MAX时,fts_accpath成员是否还包含完整路径,我继续重复这个操作,直到识别出长路径:

(lldb) x -f A $rax+0x28
0x600000fd6868: 0x00007f8fb813fa00
0x600000fd6870: 0x00007f8fb813fa00
0x600000fd6878: 0x0000000000000000
0x600000fd6880: 0x00000000006c0405
0x600000fd6888: 0x00000003000bca23
0x600000fd6890: 0x0004000401000004
0x600000fd6898: 0x0000000300000001
0x600000fd68a0: 0x0000000000000000
(lldb) x -f s 0x00007f8fb813fa00
0x7f8fb813fa00: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
warning: unable to find a NULL terminated string at 0x7f8fb813fa00.Consider increasing the maximum read length.

lldb内置了一个字符串大小限制(1024个字符),可以提示我们字符串是否超过该长度。改变这个限制值,就可以显示完整的字符串:

(lldb) setting set target.max-string-summary-length 2000
(lldb) x -f s 0x00007f8fb813fa00
0x7f8fb813fa00: "/private/var/folders/3d/4gzq8yw97cz35_55yqpv3pyh0000gn/T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
(lldb) expr (size_t) strlen(0x00007f8fb813fa00)
(size_t) $25 = 1029

现在可以澄清:该漏洞存在于对_qtn_file_apply_to_path的调用中。我对比了最近版本的macOS,想找到是否存在差异。写这篇文章时,最新版的macOS Catalina版本为10.15.7 build 19H1217,我首先检查了该系统的[BAHDecompressor _propagateQuarantineInformation]

在Hopper中打开这个版本的Archive Utility时,可以发现该方法有一点细微的差别。该方法并没有直接使用fts_open()进行迭代,而是使用Cocoa enumeratorAtURL NSFileManager API来枚举文件:

var_1C8 = *__NSConcreteStackBlock;
*(&var_1C8 + 0x10) = sub_100014422;
...
rax = [r14 enumeratorAtURL:r15 includingPropertiesForKeys:var_E0 options:0x0 errorHandler:&var_1C8];

其中有一个有趣的errorHandler代码块,包含如下代码:

int sub_100014422(int arg0, int arg1, int arg2) {
...
    if (r12 != 0x0) {
            if ([r15 isEqualToString:**_NSPOSIXErrorDomain] != 0x0) {
                    if (rbx == 0x3f) {
                            *(int8_t *)(*(*(r14 + 0x20) + 0x8) + 0x18) = 0x1;
                            r13 = 0x0;
                    }
            }
            else {
                    r13 = 0x1;
                    [r15 release];
            }
    }
...
return rax;
}

这里有一处有趣的常量比较:0x3f = 63 = ENAMETOOLONG。如果在枚举过程中碰到ENAMETOOLONG错误,那么将设置一个变量值,并且返回值表示迭代过程需要停止。在_propagateQuarantineInformation的后续逻辑中,存在一个检验过程,如果设置了一个变量,那么就会通过XPC调用一个独立的进程:

if (*(int8_t *)(var_158 + 0x18) != 0x0) {
  NSLog(@"-_propagateQuarantineInformation: applying quarantine via helper service");
  [var_F0 _propagateQuarantineInformationInService];
}

这里我认为,在迭代过程中遇到ENAMETOOLONG错误时,就会出现这种情况。(目前据我所知)这一点似乎不是特别重要,但可以告诉我们,在新版macOS中,存在一个新的helper服务,通过_propagateQuarantineInformationInService来调用。经过进一步调查,我发现AUQuarantineService属于Archive Utility的一部分。

我通过VM运行打补丁后的macOS(10.15.7 19H1208),然后将lldb附加到Archive Utility上。我在_propagateQuarantineInformation上设置了一个断点,但这个断点似乎没被触发过。然而这里AUQuarantineService这个XPC辅助服务会启动,并且根据dtruss,该服务会调用libquarantine.dylib中的_qtn_file_apply_to_path。我将调试器attach到AUQuarantineService,在_qtn_file_apply_to_path上设置了一个断点:

(lldb) bt
* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 1.1
  * frame #0: 0x00007fff6e8fa13b libquarantine.dylib`_qtn_file_apply_to_path
    frame #1: 0x000000010247c9e5 AUQuarantineService`___lldb_unnamed_symbol5$$AUQuarantineService + 138
    frame #2: 0x00007fff6e86e658 libdispatch.dylib`_dispatch_client_callout + 8
    frame #3: 0x00007fff6e87a6ec libdispatch.dylib`_dispatch_lane_barrier_sync_invoke_and_complete + 60
    frame #4: 0x000000010247c906 AUQuarantineService`___lldb_unnamed_symbol4$$AUQuarantineService + 285
    frame #5: 0x00007fff370c8657 Foundation`__NSXPCCONNECTION_IS_CALLING_OUT_TO_EXPORTED_OBJECT_S3__ + 10
...
    frame #9: 0x00007fff6eb0b13b libxpc.dylib`_xpc_connection_mach_event + 934
...
    frame #18: 0x00007fff6eac7b77 libsystem_pthread.dylib`start_wqthread + 15
(lldb) image list
[  0] ABAA25AA-8184-30A8-A6B2-D196C068DB29 0x000000010247b000 /System/Library/CoreServices/Applications/Archive Utility.app/Contents/XPCServices/AUQuarantineService.xpc/Contents/MacOS/AUQuarantineService

我们可以在Hopper中设置一个基础偏移量,或者手动计算frame #4的偏移量:0x000000010247c906 - 0x000000010247b000 = 0x1906。在Hopper中打开这个地址,可以找到一个Objective-C方法,如下所示:

/* @class AUQuarantineService */
-(void)propagateQuarantineInfo:(void *)arg2 targetURLWrapper:(void *)arg3 withReply:(void *)arg4 {
... // initialization code
    r13 = _qtn_file_alloc();
    if (r15 != 0x0) {
            rax = objc_retainAutorelease(r15);
            if (_qtn_file_init_with_data(r13, [rax bytes], [rax length]) != 0x0) {
                    _qtn_file_free(r13);
            }
            else {
                    if (r13 != 0x0) {
                            r14 = *qword_100003838;
                            var_60 = *__NSConcreteStackBlock;
                            *(&var_60 + 0x8) = 0xffffffffc2000000;
                            *(&var_60 + 0x10) = sub_10000195b; // <- block function pointer
                            *(&var_60 + 0x18) = 0x107052070;
                            *(&var_60 + 0x20) = [r12 retain];
                            *(&var_60 + 0x28) = r13;
                            dispatch_sync(r14, &var_60);
                            _qtn_file_free(r13);
                            [*(&var_60 + 0x20) release];
                    }
            }
    }
...
    return;
}

因此这个函数会调用_qtn_file_init_with_data(r13, [rax bytes], [rax length]),然后(通过dispatch_sync())调用一个栈分配块,反编译后的代码与我们之前在存在漏洞的macOS中看到的_propagateQuarantineInformation方法非常类似:

int sub_10000195b(int arg0) {
  r14 = arg0;
  var_30 = [objc_retainAutorelease(*(arg0 + 0x20)) fileSystemRepresentation];
  *(&var_30 + 0x8) = 0x0;
  rax = fts_open$INODE64(&var_30, 0x18, 0x0);
  if (rax != 0x0) {
          rbx = rax;
          rax = fts_read$INODE64(rax, 0x18, 0x0);
          if (rax != 0x0) {
                  do {
                          if (((*(int16_t *)(rax + 0x58) & 0xffff) <= 0xd) && (!COND)) {
                                  _qtn_file_apply_to_path(*(r14 + 0x28), *(rax + 0x28), 0x0);
                          }
                          rax = fts_read$INODE64(rbx);
                  } while (rax != 0x0);
          }
          fts_close$INODE64(rbx);
  }
  var_20 = **___stack_chk_guard;
  rax = *___stack_chk_guard;
  rax = *rax;
  if (rax != var_20) {
          rax = __stack_chk_fail();
  }
  return rax;
}

fts_open()fts_read()代码看上去几乎相同。为了验证这也是新版macOS中设置quarantine属性的位置,我在_qtn_file_apply_to_path上设置了一个断点,然后每次断点被触发时,我都会在调试器中观察传递给_qtn_file_apply_to_path()的第2个参数:

* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 2.1
frame #0: 0x00007fff6e8fa13b libquarantine.dylib`_qtn_file_apply_to_path
...
Target 0: (AUQuarantineService) stopped.
(lldb) x -f s $rsi
0x7f9231c0a8d8: "FakeApp.app"
(lldb) cont
...
* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 2.1
frame #0: 0x00007fff6e8fa13b libquarantine.dylib`_qtn_file_apply_to_path
...
(lldb) x -f s $rsi
0x7f9231d09538: "Contents"
(lldb) cont
...
* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 2.1
frame #0: 0x00007fff6e8fa13b libquarantine.dylib`_qtn_file_apply_to_path
...
Target 0: (AUQuarantineService) stopped.
(lldb) x -f s $rsi
0x7f9231c0ad28: "_CodeSignature"
...
...
* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 2.1
frame #0: 0x00007fff6e8fa13b libquarantine.dylib`_qtn_file_apply_to_path
libquarantine.dylib`_qtn_file_apply_to_path:
->  0x7fff6e8fa13b <+0>: pushq  %rbp
0x7fff6e8fa13c <+1>: movq   %rsp, %rbp
0x7fff6e8fa13f <+4>: pushq  %r14
0x7fff6e8fa141 <+6>: pushq  %rbx
Target 0: (AUQuarantineService) stopped.
(lldb) x -f s $rsi                                                                                                                                                      0x7f9231d099b8: "Main.storyboardc"

与存在漏洞的macOS相比,这里最关键的区别在于之前传递给_qtn_file_apply_to_path()的是一个绝对路径名,而现在传递的是一个相对路径。

如果使用相对路径,那么相对的那个路径则需要从其他地方获取。在这种情况下,前面并没有使用过opendir()或者已打开的文件目录描述符,因此使用的是当前工作目录。我们可以使用lsof来查看当前的工作目录:

root@catalina-beta-vm . # lsof -p 1140
COMMAND    PID   USER   FD   TYPE DEVICE   SIZE/OFF                NODE NAME
AUQuarant 1140 rasmus  cwd    DIR    1,4         96         12885832512 /T/com.apple.fileprovider.ArchiveService/TemporaryItems/(A Document Being Saved By ArchiveService)/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/FakeApp.app/Contents/Resources/Base.lproj
(...)

当前工作目录与$rsi寄存器包含的指针所对应的预期值相匹配,Main.storyboardc.../FakeApp.app/Contents/Resources/Base.lproj目录中。为了澄清当前工作目录如何被修改,我在fchdir()chdir()上设置了断点:

(lldb) break set -n chdir
Breakpoint 3: where = libsystem_kernel.dylib`chdir, address = 0x00007fff6ea0a314
(lldb) break set -n fchdir
Breakpoint 4: where = libsystem_kernel.dylib`fchdir, address = 0x00007fff6ea294fc
(lldb) cont
Process 1140 resuming
Process 1140 stopped
* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 4.1
    frame #0: 0x00007fff6ea294fc libsystem_kernel.dylib`fchdir
libsystem_kernel.dylib`fchdir:
->  0x7fff6ea294fc <+0>:  movl   $0x200000d, %eax          ; imm = 0x200000D 
    0x7fff6ea29501 <+5>:  movq   %rcx, %r10
    0x7fff6ea29504 <+8>:  syscall 
    0x7fff6ea29506 <+10>: jae    0x7fff6ea29510            ; <+20>
Target 0: (AUQuarantineService) stopped.
(lldb) bt
* thread #2, queue = 'com.apple.archiveutility.auquarantineservice.propogationQueue', stop reason = breakpoint 4.1
  * frame #0: 0x00007fff6ea294fc libsystem_kernel.dylib`fchdir
    frame #1: 0x00007fff6e91c35d libsystem_c.dylib`fts_safe_changedir + 90
    frame #2: 0x00007fff6e91c572 libsystem_c.dylib`fts_build + 477
    frame #3: 0x00007fff6e91c1dd libsystem_c.dylib`fts_read$INODE64 + 893
    frame #4: 0x00000001070519ed AUQuarantineService`___lldb_unnamed_symbol5$$AUQuarantineService + 146
...

因此每次目录迭代时都会调用fts_read()fchdir(),设置进程使用当前工作目录。后续调用_qtn_file_apply_to_path时只能将相对文件名作为参数,这样就不存在超出PATH_MAX限制的风险。

我们可以对比目录遍历代码,解释这种行为上的差异。在存在漏洞的版本中:

rax = fts_open$INODE64(&var_40, 0x1c, 0x0);

新版代码:

rax = fts_open$INODE64(&var_30, 0x18, 0x0);

如果查看fts_open()的man页面,可知第2个参数为带选项的位掩码。旧版代码传递的是0x1c选项,新版代码为0x18,第3个位(0x4)已被取消。查看fts.h头文件,可以看到0x4的含义:

#define FTS_NOCHDIR 0x004       /* don't change directories */

这样就有效地解决了这个问题,由于只传递文件名,因此路径不会再超过PATH_MAXFTS(3)可以确保能够安全地遍历目录层次结构,不管层次有多深。

 

0x02 总结

通过基本的系统调查、DTrace以及调试器,加上逆向分析后,我们可以确认这个漏洞的根源在于系统将绝对路径名传递给libquarantine.dylib中的qtn_file_apply_to_path(),当完整路径长度超过PATH_MAX将出现错误。修复后的系统在遍历目录结构时,会调用chdir(),只将文件名(而非完整路径)传递给qtn_file_apply_to_path()。由于macOS文件名长度最多为255个字节(也就是NAME_MAX),因此文件名本身永远不会超过PATH_MAX

由于时间有限,我还有一些点没有顾及。比如,我没有探索系统在哪个位置检查了PATH_MAX限制,我怀疑具体逻辑位于Quarantine.kext这个内核扩展所实现的syscall_quarantine_setinfo_path中(或者该函数的后续调用中)。另外,现在系统已经明显重构了Archive Utility以及该应用传播quarantine信息的方式。现在存在2个不同的目录遍历例程,一个位于Archive Utility自身中,另一个在新的XPC辅助服务AUQuarantineService中,我对这两者的使用逻辑还不清楚。我也没有探索修复后的Archive Utility在Catalina以及Big Sur上是否有所不同。

(完)