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以及文件类型判定进程(比如macprobe
、file
)后,这里还涉及到3个关键的进程:
- Archive Utility
- AUHelperService
- ArchiveService
配合fs_usage
进行分析后,很明显可以发现,zip写文件的过程由ArchiveService
负责完成。此外,根据fs_usage
,ArchiveService
会将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
中)值为0x3c
(60
),为扩展属性内容的长度。第三个参数为另一个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_MAX
。FTS(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上是否有所不同。