如何滥用MACL绕过macOS的隐私控制

 

0x00 前言

大家之前可能没听说过macOS上的MACL,因为这是macOS上比较隐蔽的一个功能,也是Apple User-Intent的基础。User-Intent是macOS隐私控制方面的一个改进,目的是避免过多的提示,干扰用户对系统的正常使用。从攻击者角度来看,过多的警报也并不是一件好事。

在端点上具备操作权限肯定是攻击者最核心的目标,然而随着macOS的迭代更新,想实现隐蔽攻击已经越来越难。即使我们已经入侵了端点,将权限提升至root,但存储在文件中的许多数据依然不可用,并且只要一步走错,就前功尽弃:

之前我曾在博客中提到,可以通过将dylib加载到特定的Apple应用entitlement中,从而绕过Apple的隐私控制(也就是所谓的TCC)。虽然这的确可以实现我们的目标,但我还是想通过其他高深的技术来应付越来越棘手的环境。

在本文中,我将与大家分享macOS中User-Intent系统的内部工作原理,分析攻击面。此外,我在研究如何滥用User-Intent功能来绕过TCC时,也找到了一个漏洞(CVE-2020-9968)。

 

0x01 User-Intent

最近在与@rbmaslen讨论黑客技术时,我们在定制TCC绕过技术的过程中观察到了一些奇怪的现象。在使用Catalina系统时,当我们打开某个应用(如Visual Studio Code),选择“File->Open”菜单打开对话框,然后选择桌面上的任何文件,点击“Open”按钮时,我们会观察到两个现象。首先,我们不会看到系统提醒的“Visual Studio Code would like to access files in your Desktop folder”对话框。另外,即使我们使用隐私设置,阻止Visual Studio Code访问桌面目录时,我们还是可以正常访问该文件。

经过一番Google后,我找到了2019年WWDC会议上的一个视频,视频中简单提及了User-Intent以及User-Consent方面的内容。从中我们可知,Apple正在尝试解决Windows系统在推行UAC功能时比较困扰用户的一些问题。

在Catalina上,Apple的解决办法是想办法让用户能够授予文件或者目录的访问权限,同时不需要每次都通过对话框来确定授权。比如,现在我们有几种方式可以确认User-Intent(即用户的操作意图):

1、用户通过Open或者Save对话框选择文件;

2、用户将文件从Finder拖拽到另一个应用窗口;

3、用户在Finder中双击一个文件。

这些逻辑也很自然,毕竟如果用户已经在对话框中显式选择了一个文件,系统没必要再次提醒用户权限问题。

接下来就是了解这些逻辑背后原理的关键点了。我花了好几天时间来了解所有环节,最终发现Apple在处理用户隐私方面,的确提供了一个令人印象深刻的系统。

我们来关注一个场景:我们构造一个小的应用,让用户通过Open对话框来选择一个文件。大家回头可以查看完整的代码,现在我们只需要关注选择文件时采用的2个方法:

// Uses the open() syscall to open a file handle
- (IBAction)clickedOpenManual:(id)sender {
    NSString *val = [_openTextBox stringValue];
    int fd = open([val cString], O_RDONLY);
    if (fd > 0) {
        [[self->_logTextBox documentView] insertText:@"Attempting to open file: Success\\n"];
    } else {
        [[self->_logTextBox documentView] insertText:@"Attempting to open file: Failure\\n"];
    }
}

// Uses the Open dialog to choose a target file
- (IBAction)clickedOpen:(id)sender {

    NSOpenPanel* panel = [NSOpenPanel openPanel];
    [panel setCanChooseDirectories:YES];

    [panel beginWithCompletionHandler:^(NSInteger result){
        if (result == NSModalResponseOK) {
            NSURL*  theDoc = [[panel URLs] objectAtIndex:0];
            [[self->_logTextBox documentView] insertText:[NSString stringWithFormat:@"Selected file: %@\\n", [theDoc path]]];
        }

    }];
}

以上示例程序为我们提供了处理文件的两种方法:在路径上调用open(),或者通过Open对话框让用户选择一个文件:

如果我们使用open调用,尝试打开桌面上的一个文件,我们可以看到熟悉的一个对话框:

如果我们选择“Don’t Allow”按钮,将导致程序无法打开该文件。

现在我们在点击Open选项前,使用Open对话框来选择目标文件。令人惊讶的是,这次一切正常,即使我们前面已经明确拒绝了程序对文件的访问:

此外,如果我们重启该app,输入一样的文件路径,同时没有通过对话框来选择目标文件,这一次open方法依然可以正常工作:

这就是典型的User-Intent工作过程,macOS发现我们使用对话框来选择文件,因此自然会赋予程序访问文件内容所需的权限,不需要附加其他警告信息。

现在一切变得非常有趣,我们可以使用如下命令,看一下是否有一些属性应用到我们选择的文件上:

xattr -l ~/Documents/supersecretz.txt

可以看到如下信息:

这里我们看到了一个com.apple.macl属性,这也是支撑这种用户体验的关键所在。

 

0x02 com.apple.macl

MACL属性通常头部值为02 00,后面为授权访问目标文件的应用对应的UUID。对于每个系统、用户以及应用程序而言,UUID都是唯一的,这意味着我们无法提前获取到该值。我不是特别确定Apple是否出于隐私考虑实现了这种功能(毕竟我们可以通过这个特征,查看用户通过应用访问了哪些文件),但无论如何,我们知道如果文件添加了MACL属性,那么匹配的应用以及用户可以重新访问该文件,不需要关心隐私设置。

这里我们举个例子,我们可以从上文的测试文件中提取已经添加的MACL信息,直接应用到另一个文件,比如~/Desktop/secret.txt

xattr -wx com.apple.macl 020091877181CB4E4D7F8004D7BFF6B58C58000000000000000000000000  ~/Desktop/secret.txt

与我们预期的一样,现在我们可以从应用中直接打开该文件,不需要关心隐私设置:

现在我们使用如下命令,移除MACL标志,再次尝试:

xattr -d com.apple.macl ~/Desktop/secret.txt

出乎意料的是,这里文件马上会重新加上MACL标志。这意味着一旦我们将MACL添加到文件中,就很难将其删除,并且不论采用何种方式来尝试删除该标志,都会在Console中看到如下信息:

 

0x03 Open对话框如何添加MACL

那么Open对话框为何如此特别,并且该对话框为何能够将MACL添加到本来无法访问的目标文件中呢?显然,我们自己的应用无法直接添加MACL,不然这将是macOS隐私控制中的一个巨大漏洞,因此肯定有其他因素在发挥作用。

我们首先来分析AppKit.framework库,这是实际负责Open对话框的一个库。我们发现该框架不仅包括AppKit库,还包含一些XPC服务:

这是Apple框架常用的一种模式,允许将entitlement分配给某个服务,以便开发者从加载到进程的库中使用XPC来请求这些服务。这里显然我们要重点关注com.apple.appkit.xpc.openAndSavePanelService.xpc这个XPC服务,观察其entitlement,我们可以看到如下信息:

这里我们可以看到一些私有的entitlement,包括非常重要的kTCCServiceSystemPolicyAllFiles entitlement,该entitlement可以在不提示用户的情况下,赋予所有文件的访问权限(包括隐私保护位置中的文件)。这跟前面我们的观察结果相符,因为macOS在添加MACL之前,需要能够访问某个文件,无视系统当前的TCC设置。那么我们是否可以肯定,这就是将MACL应用到目标文件的代码位置吗?答案是否定的。

虽然该服务的确提供了在不提示用户的情况下,访问目标文件所需的entitlement,然而对MACL属性的处理操作实际上由内核来完成,更具体一些,具体逻辑位于Sandbox.kext内核扩展中。这意味着我们需要访问内核,才能理解内部原理。

 

0x04 内核沙箱扩展

在遍历AppKit导入的符号时,我们发现其中引用了以sandbox_extension_开头的一系列函数。这些函数实际上是调用了Sandbox模块以及某个“扩展”,实现了对sandbox_ms系统调用的封装。在这个场景中,我们感兴趣的是来自libSystem.dylib的方法,包括sandbox_extension_consume以及以sandbox_extension_issue_file...开头的一些方法。

我们先来调用其中一个方法:sandbox_extension_issue_file_to_self,这个函数需要3个参数:

char* sandbox_extension_issue_file_to_self(const char *sandboxEnt, const char *filePath, int flags);

调用该函数,传入我们可以访问的目标文件的路径后,函数会返回如下一个字符串:

f6b75b461bada9c3b2e73400359bc8d5844b4a3d4442700fd352fe45bcfd650b;00;00030000;00001b03;003d0fe5;000000000000001a;com.apple.app-sandbox.read;01;01000005;00000000000d1bbf;1d;/users/xpn/documents

这到底是啥?为了理解其中含义,我们需要启动反汇编器,跳转到Sandbox.kext

我们感兴趣的函数是_syscall_extension_issue,该函数需要提供2个参数:

_syscall_extension_issue(proc_t *proc, struct extension_issue_request *req);

一番逆向分析后,我们发现第二个参数似乎为满足如下格式的一个结构:

struct extension_issue_request {
  const char *sandbox_string; // 0x00
  int cmd; // 0x08
  const char *filePath; // 0x10
  int flags; // 0x18
  char *returnedToken; // 0x20
  int pid; // 0x28
  int res; // 0x30
}

如果继续逆向分析该函数,我们可以解析出前面那串令牌中比较有趣的部分:

f6b75b461bada9c3b2e73400359bc8d5844b4a3d4442700fd352fe45bcfd650b - HMAC-SHA256 of the token
00 - Sandbox extension cmd
00030000 - Flags
00001b03 - PID
003d0fe5 - PID Version
000000000000001a - Size of sandbox string
com.apple.app-sandbox.read - Sandbox string
01 - Does file exist
01000005 - Filesystem ID
00000000000d1bbf - iNode ID
1d - Sandbox Storage Class
/users/xpn/documents - Target file path

在继续分析之前,我们先来讨论下HMAC-SHA256哈希。用于HMAC的秘钥实际上是在加载sandbox.kext模块时生成的,这意味着在系统重启后,原来的令牌将会失效。

秘钥长度为随机的40个字节,存储在_secret变量中,因此暴力破解也不大可能:

那么我们要如何处理返回的这个令牌?为了理解这个令牌的处理方式,我们需要分析另一个方法:_syscall_extension_consume,该函数接受两个参数:

_syscall_extension_consume(proc_t *proc, struct extension_consume_request *req);

反汇编该函数后,我们发现传入的req结构如下所示:

struct extension_consume_request {
  const char *token;
  int length;
  int *returnValue;
};

那么当令牌被解析时,系统会执行哪些校验?首先是对HMAC-SHA256哈希的校验:

这里顺便提一下,如果有人想对HMAC哈希发起时序攻击,恢复出秘钥的话,Apple早就考虑过这种场景:

如果HMAC-SHA256哈希值匹配,接下来系统就会验证PID以及PID版本是否与我们的调用进程相匹配:

如果满足条件,接下来我们会根据令牌中包含的Sandbox扩展cmd值,进入某条代码分支(总共4条分支)。在我们的测试用例中,这个值为0x00,因此会调用_macl_record函数,使用如下参数:

_macl_record(proc_t *proc, const char *filename, bool fileExists, int fsID, int iNode, int res);

这个函数会对传入的参数执行多想检查。首先,函数会根据传入的文件系统ID确认是否存在inode

如果该文件存在,则会添加MACL:

有趣的是,如果通过inode发现目标文件不存在,系统就会根据文件名来查找文件,如果文件名存在,则会应用MACL。

现在我们已经弄清整个处理路径,从User-Intent框架,到XPC驱动的对话框,到内核,到MACL,现在我们可以开始找一些bug。

 

0x05 滥用User-Intent绕过TCC

了解了User-Intent的工作原理后,我们是否有方法能够滥用这个特性来绕过TCC。在了解系统内部工作原理后,我花了几个小时,终于找到了一种滥用方法。

我能找到的一种最简单的方法是,通过chroot容器将MACL分配给我们不具备访问权限的一个目录。这意味着我们需要具备root或者sudo权限才能完成该任务,因为我们需要该权限才能调用chroot。不论如何,我们先使用如下方式来创建一个简单的容器:

# Add libs and progs required by our POC
mkdir -p /tmp/jail/usr/lib/; cp -r /usr/lib/* /tmp/jail/usr/lib/
mkdir /tmp/jail/bin; cp /bin/bash /tmp/jail/bin

# Add our POC
cp /tmp/poc /tmp/jail/

构造好容器后,我们可以使用如下命令进入:

# Execute our chroot to grab a token
sudo chroot /tmp/jail /bin/sh -c "mkdir -p /Users/xpn/Documents; /main issue /Users/xpn/Documents"

执行完毕后,我们可以得到如下信息:

我们拿到了/Users/xpn/Documentschroot路径对应的一个令牌,现在如果我们尝试使用该令牌会怎么样呢?

不幸的是,这种方式无法成功。这是因为我们的inode值仍然存在,并且根据前文分析,如果inode存在,那么它的优先级会高于文件名。因此我们来调整命令,在令牌生成后删除该目录:

sudo chroot /tmp/jail /bin/sh -c "mkdir -p /Users/xpn/Desktop; /main issue /Users/xpn/Desktop; rmdir /Users/xpn/Desktop"

这一次如果我们使用这个令牌,可以看到我们拿到了Documents目录的访问权限,完全绕过了TCC:

这种方式当然也适用于受TCC保护的任何文件夹。

备注:我发现rmdir需要从chroot内部执行,但我还没有弄清其中的原因。如果大家想执行rm /tmp/jail/Users/xpn/Desktop之类的操作,那么应用令牌后并不能够访问受保护的文件夹。我还不知道为什么会出现这种情况,欢迎大家一起来讨论。

 

0x06 总结

希望阅读本文后,大家能了解为什么TCC有时候可以会允许已被阻止的操作。虽然本文讨论了一个简单的bug,但分析过程也颇费心思,需要对macOS内部原理有所了解才能解开谜团。

从macOS 10.15.6、iOS 14、WatchOS 7以及tvOS 14开始,这个bug已经被修复。

(完)