0x00 前言

近期,我在SaltStack的Salt中发现了一个命令注入漏洞,当master调用restartcheck时,该漏洞允许通过特制的进程名称对minion进行特权提升。

受影响的版本:自2016.3.0rc2到3002.5之间的所有版本

CVSS评分:7.0 高危

官方公告:https://saltproject.io/security_announcements/active-saltstack-cve-release-2021-feb-25/

PoC:https://github.com/stealthcopter/CVE-2020-28243

在这篇文章中,我将重点介绍以下几部分内容:

1、发现:我是如何发现这个漏洞的?

2、漏洞详情:为什么这个漏洞有效?

3、利用方式:应该如何利用这个漏洞?

4、安全问题:包括容器逃逸、未经身份验证的远程代码执行

5、时间节点:这一过程花了多长时间?

0x01 关于SaltStack Salt

SaltStack Salt是一个流行工具,用于自动化和保护基础结构,其使用过程分为两个角色。其中,一个系统设置为master,负责控制连接到该系统的其他系统。其中的一个或多个系统被设置为minion,与master连接并响应其发出的任意命令。

master和minion通常都是以root身份在其安装的系统上运行。

0x02 漏洞发现

在查看SaltStack的源代码并寻找安全漏洞的过程中,我选择使用Bandit(一个针对Python应用程序的安全扫描程序)运行源代码,并查看其发现的问题。

我以为会看到很少的漏洞,因为SaltStack的代码库较大,并且已经维护了几年的时间。但是,从下面的截图中可以看到,Bandit展示的问题数量比我预期的还要多,其中包括117个高危问题。

Bandit针对SaltStack Salt的检测报告:

1.png

当然,其中的很多问题都是误报,或者对我们来说意义不大,并且可能需要花费大量时间来逐一进行分析。为了快速找到关键漏洞,我决定将精力聚焦在几个子进程实例可能产生的命令注入漏洞。我将Popen与代码库中的shell=True结合使用。

在分析其中的一些漏洞的过程中,我尝试了多种方法,起初都无法控制这些漏洞。但后来,我发现可以利用进程名称,以一种非常巧妙的欺骗手段来控制它们。

2.png

这也就是我们正在寻找的漏洞。

0x03 漏洞详情

minion在重新启动检查的过程中存在命令注入漏洞。当某个进程打开的文件描述符以(deleted)为结尾时(需要加上前面的空格),攻击者可以通过精心设计进程名称的方式实现命令注入。这样一来,就允许任意用户在minion任何未明确禁止的目录中创建文件,实现到root的本地特权提升。

3.1 漏洞代码

存在漏洞的代码位于restartcheck.py的第615行,其中使用shell=True和被攻击者操纵的命令来调用subprocess.Popen。

cmd = cmd_pkg_query + package
paths = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)

根据操作系统的不同,软件包是由进程名称组成的,cmd_pkg_query可能如下:

Debian:dpkg-query --listfiles

RedHat:repoquery -l

NILinuxRT:opkg files

如果我们能够插入bash控制字符到进程名称中(例如; | &&等),就可以在代码到达上述一行时触发注入。

但是,要到达这一行代码,进程首先需要打开文件处理程序,打开以(deleted)为结尾的文件,并且该文件必须位于未明确禁止的目录中。

被禁止的目录列表请参考这里。这个列表可能会排除掉一些我们可能会尝试的位置,例如/tmp和/dev/shm。但是,特权较低的用户可能会访问一些常用的位置,例如:

/var/crash

/var/spool/samba

/var/www

3.2 进程名称

在我们的研究过程中,事实证明,要可靠地修改进程名称是一件比较难的事情。并且,ps列出的进程名称可能会与Python psutil库返回的名称不同。

在Linux中,进程名称可以包含任何字符(除null外)。任何用户都可以在系统上启动进程,并且进程本身可以设置进程的名称。因此,它们就成为了命令注入漏洞的良好目标,因为开发人员不太可能会想到进程名称包含特殊字符或注入的场景。

我们可以使用exec -a直接设置进程名称。但是,这种方式在busybox或sh shell中是不起作用的,并且在使用psutil时似乎没有显示出相同的名称。当然,我们也可以通过直接操作procfs来修改进程名称,但是这种方法也会导致结果不一致。

因此,我们最后找到的最简单、最准确的方法是重命名正在运行的二进制文件或脚本。由于Linux的文件名中不能包含/字符,所以就限制了我们可以注入的命令。不过,使用Base64编码可以忽略此限制,如下所示:

# 如果我们想将shadow文件复制到/tmp,我们可以运行 
cp /etc/shadow /tmp/shadow
# 将其转换为Base64字符串
echo cp /etc/shadow /tmp/shadow | base64 -w0
# 转换结果
Y3AgL2V0Yy9zaGFkb3cgL3RtcC9zaGFkb3cK
# 我们需要运行的新命令
echo Y3AgL2V0Yy9zaGFkb3cgL3RtcC9zaGFkb3cK|base64 -d|sh -i

0x04 漏洞利用

PoC脚本exploit.sh的用法:

3.png

为了成功利用这个漏洞,需要使用一个未被SaltStack明确禁止的可写目录。在不带参数运行PoC脚本后,将会搜索符合该限制的目录。

示例脚本输出了潜在可写入的目录:

4.png

随后,使用-w标志加上选定的可写目录,使用-c标志加上命令,将会创建一个进程,在该进程的名称中包含命令注入和一个打开的文件处理程序,当master进入该文件处理程序时,漏洞将被处罚,并调用restartcheck。

我们逐步执行漏洞利用过程以展示这一漏洞。为了尝试,我们可以以root用户身份创建一个简单的文件。首先,带上特定标志运行脚本。

运行漏洞脚本,其中标记了进程名称和文件处理程序:

5.png

既然已经确定恶意进程正在运行,并且名称中已经注入了命令,同时也打开了文件处理程序,那么我们就可以在SaltStack的master发出restartcheck.restartcheck命令。完成此操作后,可以检查根目录中是否存在被黑的文件。

6.png

上图证明漏洞利用已经成功,我们可以以root用户身份执行代码。

不错,接下来我们可以做一些更酷的事情。如何获得一个root Shell呢?在下面的视频中,我们通过复制find二进制文件,并将其设置为suid,最终成功获得了root身份的Shell。

视频:https://asciinema.org/a/382327

对于其他场景中,下图从master视角展现了漏洞利用的过程。

来自master POV的命令注入,其中标出了注入的对应行:

7.png'

0x05 其他安全问题

上面讨论了在本地利用漏洞实现到root用户特权提升的场景。但是,在此过程中我们还应该考虑一些其他安全问题。

5.1 容器逃逸

我们在主机上已经列出了容器化的进程,因此可以从容器内部执行这一漏洞利用,从而在主机上以root用户身份实现命令执行。

上图展示进程在容器中启动,下图为主机上使用ps列出的容器进程:

8.png

5.2 非特权远程代码执行

尽管不太可能,但这样的漏洞还是有机会被不具备本地Shell访问权限的攻击者利用。原因在于,在某些情况下,远程用户可以影响进程的名称。

0x06 时间节点

SaltStack的响应速度非常快,我发现和披露该漏洞的完整时间节点如下。

2020年11月5日 发现漏洞

2020年11月5日 通知SaltStack

2020年11月5日 SaltStack开始调查

2020年11月6日 分配CVE ID

2020年11月7日 SaltStack确认漏洞

2020年11月18日 SaltStack通知计划在1月发布修复程序

2021年1月22日 宣布将于2月4日发布安全修复程序

2021年2月4日 安全修复程序发布日期延迟至2月25日

2021年2月25日 已发布安全修复程序(+91天)

0x07 总结

在开发和维护过程中,要保证代码的安全性已经是非常复杂的工作了。不难理解,开发人员很难想到恶意进程名称的这个维度。如果大家有任何问题或者反馈,欢迎在Twitter上与我联系。同时,这也是我的第一个CVE,还请大家温柔一些。

0x00 前言

近期,我在SaltStack的Salt中发现了一个命令注入漏洞,当master调用restartcheck时,该漏洞允许通过特制的进程名称对minion进行特权提升。

受影响的版本:自2016.3.0rc2到3002.5之间的所有版本

CVSS评分:7.0 高危

官方公告:https://saltproject.io/security_announcements/active-saltstack-cve-release-2021-feb-25/

PoC:https://github.com/stealthcopter/CVE-2020-28243

在这篇文章中,我将重点介绍以下几部分内容:

1、发现:我是如何发现这个漏洞的?

2、漏洞详情:为什么这个漏洞有效?

3、利用方式:应该如何利用这个漏洞?

4、安全问题:包括容器逃逸、未经身份验证的远程代码执行

5、时间节点:这一过程花了多长时间?

0x01 关于SaltStack Salt

SaltStack Salt是一个流行工具,用于自动化和保护基础结构,其使用过程分为两个角色。其中,一个系统设置为master,负责控制连接到该系统的其他系统。其中的一个或多个系统被设置为minion,与master连接并响应其发出的任意命令。

master和minion通常都是以root身份在其安装的系统上运行。

0x02 漏洞发现

在查看SaltStack的源代码并寻找安全漏洞的过程中,我选择使用Bandit(一个针对Python应用程序的安全扫描程序)运行源代码,并查看其发现的问题。

我以为会看到很少的漏洞,因为SaltStack的代码库较大,并且已经维护了几年的时间。但是,从下面的截图中可以看到,Bandit展示的问题数量比我预期的还要多,其中包括117个高危问题。

Bandit针对SaltStack Salt的检测报告:

1.png

当然,其中的很多问题都是误报,或者对我们来说意义不大,并且可能需要花费大量时间来逐一进行分析。为了快速找到关键漏洞,我决定将精力聚焦在几个子进程实例可能产生的命令注入漏洞。我将Popen与代码库中的shell=True结合使用。

在分析其中的一些漏洞的过程中,我尝试了多种方法,起初都无法控制这些漏洞。但后来,我发现可以利用进程名称,以一种非常巧妙的欺骗手段来控制它们。

2.png

这也就是我们正在寻找的漏洞。

0x03 漏洞详情

minion在重新启动检查的过程中存在命令注入漏洞。当某个进程打开的文件描述符以(deleted)为结尾时(需要加上前面的空格),攻击者可以通过精心设计进程名称的方式实现命令注入。这样一来,就允许任意用户在minion任何未明确禁止的目录中创建文件,实现到root的本地特权提升。

3.1 漏洞代码

存在漏洞的代码位于restartcheck.py的第615行,其中使用shell=True和被攻击者操纵的命令来调用subprocess.Popen。

cmd = cmd_pkg_query + package
paths = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)

根据操作系统的不同,软件包是由进程名称组成的,cmd_pkg_query可能如下:

Debian:dpkg-query --listfiles

RedHat:repoquery -l

NILinuxRT:opkg files

如果我们能够插入bash控制字符到进程名称中(例如; | &&等),就可以在代码到达上述一行时触发注入。

但是,要到达这一行代码,进程首先需要打开文件处理程序,打开以(deleted)为结尾的文件,并且该文件必须位于未明确禁止的目录中。

被禁止的目录列表请参考这里。这个列表可能会排除掉一些我们可能会尝试的位置,例如/tmp和/dev/shm。但是,特权较低的用户可能会访问一些常用的位置,例如:

/var/crash

/var/spool/samba

/var/www

3.2 进程名称

在我们的研究过程中,事实证明,要可靠地修改进程名称是一件比较难的事情。并且,ps列出的进程名称可能会与Python psutil库返回的名称不同。

在Linux中,进程名称可以包含任何字符(除null外)。任何用户都可以在系统上启动进程,并且进程本身可以设置进程的名称。因此,它们就成为了命令注入漏洞的良好目标,因为开发人员不太可能会想到进程名称包含特殊字符或注入的场景。

我们可以使用exec -a直接设置进程名称。但是,这种方式在busybox或sh shell中是不起作用的,并且在使用psutil时似乎没有显示出相同的名称。当然,我们也可以通过直接操作procfs来修改进程名称,但是这种方法也会导致结果不一致。

因此,我们最后找到的最简单、最准确的方法是重命名正在运行的二进制文件或脚本。由于Linux的文件名中不能包含/字符,所以就限制了我们可以注入的命令。不过,使用Base64编码可以忽略此限制,如下所示:

# 如果我们想将shadow文件复制到/tmp,我们可以运行 
cp /etc/shadow /tmp/shadow
# 将其转换为Base64字符串
echo cp /etc/shadow /tmp/shadow | base64 -w0
# 转换结果
Y3AgL2V0Yy9zaGFkb3cgL3RtcC9zaGFkb3cK
# 我们需要运行的新命令
echo Y3AgL2V0Yy9zaGFkb3cgL3RtcC9zaGFkb3cK|base64 -d|sh -i

0x04 漏洞利用

PoC脚本exploit.sh的用法:

3.png

为了成功利用这个漏洞,需要使用一个未被SaltStack明确禁止的可写目录。在不带参数运行PoC脚本后,将会搜索符合该限制的目录。

示例脚本输出了潜在可写入的目录:

4.png

随后,使用-w标志加上选定的可写目录,使用-c标志加上命令,将会创建一个进程,在该进程的名称中包含命令注入和一个打开的文件处理程序,当master进入该文件处理程序时,漏洞将被处罚,并调用restartcheck。

我们逐步执行漏洞利用过程以展示这一漏洞。为了尝试,我们可以以root用户身份创建一个简单的文件。首先,带上特定标志运行脚本。

运行漏洞脚本,其中标记了进程名称和文件处理程序:

5.png

既然已经确定恶意进程正在运行,并且名称中已经注入了命令,同时也打开了文件处理程序,那么我们就可以在SaltStack的master发出restartcheck.restartcheck命令。完成此操作后,可以检查根目录中是否存在被黑的文件。

6.png

上图证明漏洞利用已经成功,我们可以以root用户身份执行代码。

不错,接下来我们可以做一些更酷的事情。如何获得一个root Shell呢?在下面的视频中,我们通过复制find二进制文件,并将其设置为suid,最终成功获得了root身份的Shell。

视频:https://asciinema.org/a/382327

对于其他场景中,下图从master视角展现了漏洞利用的过程。

来自master POV的命令注入,其中标出了注入的对应行:

7.png'

0x05 其他安全问题

上面讨论了在本地利用漏洞实现到root用户特权提升的场景。但是,在此过程中我们还应该考虑一些其他安全问题。

5.1 容器逃逸

我们在主机上已经列出了容器化的进程,因此可以从容器内部执行这一漏洞利用,从而在主机上以root用户身份实现命令执行。

上图展示进程在容器中启动,下图为主机上使用ps列出的容器进程:

8.png

5.2 非特权远程代码执行

尽管不太可能,但这样的漏洞还是有机会被不具备本地Shell访问权限的攻击者利用。原因在于,在某些情况下,远程用户可以影响进程的名称。

0x06 时间节点

SaltStack的响应速度非常快,我发现和披露该漏洞的完整时间节点如下。

2020年11月5日 发现漏洞

2020年11月5日 通知SaltStack

2020年11月5日 SaltStack开始调查

2020年11月6日 分配CVE ID

2020年11月7日 SaltStack确认漏洞

2020年11月18日 SaltStack通知计划在1月发布修复程序

2021年1月22日 宣布将于2月4日发布安全修复程序

2021年2月4日 安全修复程序发布日期延迟至2月25日

2021年2月25日 已发布安全修复程序(+91天)

0x07 总结

在开发和维护过程中,要保证代码的安全性已经是非常复杂的工作了。不难理解,开发人员很难想到恶意进程名称的这个维度。如果大家有任何问题或者反馈,欢迎在Twitter上与我联系。同时,这也是我的第一个CVE,还请大家温柔一些。

1.png

概述

由于目前VMware vCenter远程代码执行(CVE-2021-21972)的PoC已经发布,因此我们将发布包含所有技术细节的文章。

在2020年秋季,我在VMware vCenter的vSphere Client组件中发现了几个漏洞。这些漏洞导致未经授权的客户端可以通过多种协议代表目标服务器执行任意命令并发送请求:

· 未经授权的文件上传导致远程代码执行漏洞(CVE-2021-21972)

· 未经授权的服务器端请求伪造(SSRF)漏洞(CVE-2021-21973)

在本文中,我将详细回顾我是如何发现VMware vSphere客户端远程代码执行漏洞的,同时披露技术细节,并说明如何在多个平台上实现漏洞利用。

关于VMware vCenter/vSphere:

vSphere和vCenter用于企业基础架构的虚拟化,并提供一种有效的控制方式。我们有可能在公网发现该软件,但在大多数情况下,它还是位于内网之中。

发现漏洞

在对vSphere Client进行分析的过程中,我习惯地采用了白盒和黑盒两种方法进行测试,重点关注未经授权即可利用的漏洞。在Web面板中,我尝试发送了尽可能多的不同请求,这些请求中不包含Cookie标头。

当我向/ui/vropspluginui/rest/services/*发送未经授权的请求时,我发现它实际上不需要经过任何身份验证。

未经授权即可访问该URL:

2.png

该Web应用程序的某些功能依赖于通常位于独立.jar文件中的插件。例如,vropspluginui插件在文件vropsplugin-service.jar中实现。

就我的理解而言,每个插件必须在Web面板中指定哪些终端需要授权才能运行、哪些终端不需要授权。该插件被配置为允许未经授权的用户访问其处理的任何URL。

其中,/ui/vropspluginui/rest/services/uploadova URL对应的uploadOvaFile函数引起了我的关注。

代码漏洞部分:

3.png

此路径的处理程序执行以下操作:

1. 接收带有uploadFile参数的POST请求;

2. 读取此参数的内容并写入inputStream变量;

3. 以.tar压缩包格式打开生成的数据;

4. 检索所有压缩的条目(非目录条目);

5. 在遍历所有条目时,使用文件命名约定/tmp/unicorn_ova_dir + entry_name在磁盘上创建当前条目的副本。

在这里,我注意到了.tar条目名称没有进行过滤。它们直接与字符串“/tmp/unicorn_ova_dir”连接,并在连接后的路径位置创建一个文件。这意味着,我们可以创建一个包含字符串“../”的压缩条目,这将允许我们将任意文件上传到服务器上的任意目录。

为了利用这个漏洞来构建.tar压缩包,我首先使用了evilarc实用程序。这是这个实用程序第二次派上用场,第一次也是在与之类似的漏洞研究过程中利用。

python evilarc.py -d 2 -p 'testFolder\' -o win -f winexpl.tar testUpload.txt

生成的压缩文件中,包含名称为“..\..\testFolder\testUpload.txt”的文件。我将其上传到URL “/ui/vropspluginui/rest/services/uploadova”中,并检查服务器的文件系统功能中国年是否存在testFolder文件夹,其嵌套文件位于C:\根目录中。

POST /ui/vropspluginui/rest/services/uploadova HTTP/1.1
Host: vSphereClient.local
Connection: close
Accept: application/json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryH8GoragzRFVTw1VD
Content-Length: 10425
------WebKitFormBoundaryH8GoragzRFVTw1VD
Content-Disposition: form-data; name="uploadFile"; filename="a.ova"
Content-Type: text/plain
{craftedArchive}
------WebKitFormBoundaryH8GoragzRFVTw1VD--

测试文件已经成功上传:

4.png

可以看到,我的.txt文件已经成功上传,现在的路径为C:\testFolder\testUpload.txt。

实现Windows RCE

为了能够在目标系统上执行任意命令,我们需要上传一个.jsp Shell,这个Shell无需授权即可访问。要发现这样的位置,我们需要:

1. 找到磁盘上可以利用上述漏洞创建文件的可写路径;

2. 将找到的文件路径映射到可访问的Web根目录的文件夹结构中,使该目录能够运行.jsp脚本,并且不需要授权。

首先,我们先上传文件testUpload.txt并查看其属性菜单,以确认我们上传的文件获得了哪些特权。我们可以看到其所有者是用户“vsphere-ui”。

上传文件的属性:

5.png

在搜索目标位置时,我们发现了C:\ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport\(其中存在.jsp文件),这个目录看起来很有希望。

无需授权即可访问JSP脚本:

6.png

经过确认,我们在未授权的情况下可以访问该jsp脚本。接下来,我们检查一下vsphere-ui是否对该目录具有写入权限。

目标文件夹的安全相关属性:

7.png

答案是肯定的。现在,我们就可以上传特制的.jsp文件,以在系统上执行命令。

我们可以创建一个包含特制.jsp Shell Payload的压缩包,将其发送到上述所分析的URL。

python evilarc.py -d 5 -p 'ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport' -o win -f winexpl.tar testRCE.jsp

漏洞利用:

8.png


现在,我们的.jsp脚本已经上传到服务器,这就让我们有机会以NT AUTHORITY\SYSTEM特权在系统上执行任意命令。

实现Linux RCE

在Linux场景中,漏洞利用方式有所不同,但Linux也非常容易遭到攻击,并允许外部用户上传任意文件。

在Linux上,我没有找到同时允许上传和执行.jsp Shell的目录。不过,还有另一种在服务器上实现命令执行的方法。

我们知道,我们可以使用vsphere-ui用户的权限上传任意文件。如果我们将公钥上传到该用户的主目录,并尝试使用私钥通过SSH连接到服务器,这样是否可行?

我们首先确认SSH是否可以从外部访问:

nmap -p 22 vSphereLinux.local

发现目标端口已打开:

9.png

第一步是要生成密钥对。

ssh-keygen -t rsa

生成密钥对:

10.png

然后,使用生成的公钥,创建一个.tar压缩文件:

python evilarc.py -d 5 -p 'home/vsphere-ui/.ssh' -o unix -f linexpl.tar authorized_keys

使用Evilarc生成tar压缩包:

11.png

接下来,我们利用这个漏洞上传文件,并尝试通过SSH连接到目标主机:

ssh -i /path/to/id_rsa [email protected]

进入到命令行:

12.png

至此,我们就可以使用vsphere-ui用户的权限访问服务器。

总结

在本文中,我演示了一种以未经身份验证的用户在VMware vSphere Client中实现远程代码执行的方法。由于vropspluginui插件未进行充分的身份验证,所以除了进入到命令行之外,攻击者还可以执行其他恶意操作。

我们强烈建议大家立即更新到最新版本的VMware vSphere Client。关于该漏洞的其他信息,请参考iVMSA-2021-0002。

希望这篇文章能有助于大家对漏洞的理解。

时间节点

2020年10月2日 向厂商报告漏洞

2020年10月3日 厂商第一次回复

2020年10月9日 成功复现漏洞,厂商开始制定修复计划

2021年2月23日 修复漏洞并发布安全公告

1.png

概述

由于目前VMware vCenter远程代码执行(CVE-2021-21972)的PoC已经发布,因此我们将发布包含所有技术细节的文章。

在2020年秋季,我在VMware vCenter的vSphere Client组件中发现了几个漏洞。这些漏洞导致未经授权的客户端可以通过多种协议代表目标服务器执行任意命令并发送请求:

· 未经授权的文件上传导致远程代码执行漏洞(CVE-2021-21972)

· 未经授权的服务器端请求伪造(SSRF)漏洞(CVE-2021-21973)

在本文中,我将详细回顾我是如何发现VMware vSphere客户端远程代码执行漏洞的,同时披露技术细节,并说明如何在多个平台上实现漏洞利用。

关于VMware vCenter/vSphere:

vSphere和vCenter用于企业基础架构的虚拟化,并提供一种有效的控制方式。我们有可能在公网发现该软件,但在大多数情况下,它还是位于内网之中。

发现漏洞

在对vSphere Client进行分析的过程中,我习惯地采用了白盒和黑盒两种方法进行测试,重点关注未经授权即可利用的漏洞。在Web面板中,我尝试发送了尽可能多的不同请求,这些请求中不包含Cookie标头。

当我向/ui/vropspluginui/rest/services/*发送未经授权的请求时,我发现它实际上不需要经过任何身份验证。

未经授权即可访问该URL:

2.png

该Web应用程序的某些功能依赖于通常位于独立.jar文件中的插件。例如,vropspluginui插件在文件vropsplugin-service.jar中实现。

就我的理解而言,每个插件必须在Web面板中指定哪些终端需要授权才能运行、哪些终端不需要授权。该插件被配置为允许未经授权的用户访问其处理的任何URL。

其中,/ui/vropspluginui/rest/services/uploadova URL对应的uploadOvaFile函数引起了我的关注。

代码漏洞部分:

3.png

此路径的处理程序执行以下操作:

1. 接收带有uploadFile参数的POST请求;

2. 读取此参数的内容并写入inputStream变量;

3. 以.tar压缩包格式打开生成的数据;

4. 检索所有压缩的条目(非目录条目);

5. 在遍历所有条目时,使用文件命名约定/tmp/unicorn_ova_dir + entry_name在磁盘上创建当前条目的副本。

在这里,我注意到了.tar条目名称没有进行过滤。它们直接与字符串“/tmp/unicorn_ova_dir”连接,并在连接后的路径位置创建一个文件。这意味着,我们可以创建一个包含字符串“../”的压缩条目,这将允许我们将任意文件上传到服务器上的任意目录。

为了利用这个漏洞来构建.tar压缩包,我首先使用了evilarc实用程序。这是这个实用程序第二次派上用场,第一次也是在与之类似的漏洞研究过程中利用。

python evilarc.py -d 2 -p 'testFolder\' -o win -f winexpl.tar testUpload.txt

生成的压缩文件中,包含名称为“..\..\testFolder\testUpload.txt”的文件。我将其上传到URL “/ui/vropspluginui/rest/services/uploadova”中,并检查服务器的文件系统功能中国年是否存在testFolder文件夹,其嵌套文件位于C:\根目录中。

POST /ui/vropspluginui/rest/services/uploadova HTTP/1.1
Host: vSphereClient.local
Connection: close
Accept: application/json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryH8GoragzRFVTw1VD
Content-Length: 10425
------WebKitFormBoundaryH8GoragzRFVTw1VD
Content-Disposition: form-data; name="uploadFile"; filename="a.ova"
Content-Type: text/plain
{craftedArchive}
------WebKitFormBoundaryH8GoragzRFVTw1VD--

测试文件已经成功上传:

4.png

可以看到,我的.txt文件已经成功上传,现在的路径为C:\testFolder\testUpload.txt。

实现Windows RCE

为了能够在目标系统上执行任意命令,我们需要上传一个.jsp Shell,这个Shell无需授权即可访问。要发现这样的位置,我们需要:

1. 找到磁盘上可以利用上述漏洞创建文件的可写路径;

2. 将找到的文件路径映射到可访问的Web根目录的文件夹结构中,使该目录能够运行.jsp脚本,并且不需要授权。

首先,我们先上传文件testUpload.txt并查看其属性菜单,以确认我们上传的文件获得了哪些特权。我们可以看到其所有者是用户“vsphere-ui”。

上传文件的属性:

5.png

在搜索目标位置时,我们发现了C:\ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport\(其中存在.jsp文件),这个目录看起来很有希望。

无需授权即可访问JSP脚本:

6.png

经过确认,我们在未授权的情况下可以访问该jsp脚本。接下来,我们检查一下vsphere-ui是否对该目录具有写入权限。

目标文件夹的安全相关属性:

7.png

答案是肯定的。现在,我们就可以上传特制的.jsp文件,以在系统上执行命令。

我们可以创建一个包含特制.jsp Shell Payload的压缩包,将其发送到上述所分析的URL。

python evilarc.py -d 5 -p 'ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport' -o win -f winexpl.tar testRCE.jsp

漏洞利用:

8.png


现在,我们的.jsp脚本已经上传到服务器,这就让我们有机会以NT AUTHORITY\SYSTEM特权在系统上执行任意命令。

实现Linux RCE

在Linux场景中,漏洞利用方式有所不同,但Linux也非常容易遭到攻击,并允许外部用户上传任意文件。

在Linux上,我没有找到同时允许上传和执行.jsp Shell的目录。不过,还有另一种在服务器上实现命令执行的方法。

我们知道,我们可以使用vsphere-ui用户的权限上传任意文件。如果我们将公钥上传到该用户的主目录,并尝试使用私钥通过SSH连接到服务器,这样是否可行?

我们首先确认SSH是否可以从外部访问:

nmap -p 22 vSphereLinux.local

发现目标端口已打开:

9.png

第一步是要生成密钥对。

ssh-keygen -t rsa

生成密钥对:

10.png

然后,使用生成的公钥,创建一个.tar压缩文件:

python evilarc.py -d 5 -p 'home/vsphere-ui/.ssh' -o unix -f linexpl.tar authorized_keys

使用Evilarc生成tar压缩包:

11.png

接下来,我们利用这个漏洞上传文件,并尝试通过SSH连接到目标主机:

ssh -i /path/to/id_rsa [email protected]

进入到命令行:

12.png

至此,我们就可以使用vsphere-ui用户的权限访问服务器。

总结

在本文中,我演示了一种以未经身份验证的用户在VMware vSphere Client中实现远程代码执行的方法。由于vropspluginui插件未进行充分的身份验证,所以除了进入到命令行之外,攻击者还可以执行其他恶意操作。

我们强烈建议大家立即更新到最新版本的VMware vSphere Client。关于该漏洞的其他信息,请参考iVMSA-2021-0002。

希望这篇文章能有助于大家对漏洞的理解。

时间节点

2020年10月2日 向厂商报告漏洞

2020年10月3日 厂商第一次回复

2020年10月9日 成功复现漏洞,厂商开始制定修复计划

2021年2月23日 修复漏洞并发布安全公告

0x00 概述

iMessage是整个Apple生态系统中广泛使用的安全消息传递应用程序和协议。由于对在其他平台上运行的iMessage原理感到好奇,我们使用了一种逆向工程的方式来探究iMessage的运行方式,并研究将其扩展到其他平台的可能性。

本文的目标是说明Apple如何利用其硬件产品来保护其软件。为了对此进行探讨,我们将尝试通过Apple Push Notification(APN)直接在网络级别进行连接,并解决遇到的问题。在这个过程中,我们将使用流行的开源工具对macOS上apsd守护程序的一部分以及APN协议自身进行逆向工程。

0x01 当前解决方案

在当前,如果要在Apple生态系统之外运行iMessage,就必须使用Mac服务器,并依靠AppleScript脚本来自动执行Messages.app UI操作。这样一来,就无需再客户端上重新实现消息发送协议。但是,这里最大的问题在于,如果要使用iMessage,则Mac必须一直运行。

与针对独立二进制文件进行逆向有所不同,iMessage发送代码(像XNU OS中大多数内部函数一样)已经超出了Messages.app的范围,并且该进程依赖于许多系统守护程序,即微服务体系结构,并且它们依赖XPC消息作为IPC(进程间通信)机制。

Project Zero已经对iMessage中涉及的守护程序结构进行了充分的研究,因此在这篇文章中我们将省略不必要的细节。简而言之,如果我们编辑好一条消息,并按下回车键,就会经历Messages.app -> imagent -> identityservicesd -> apsd的过程。为了进一步对这个过程进行分析,我编写了两个基于Frida的工具,并在此过程中遇到了两大挑战。

首先,在反汇编程序中静态查找ObjC方法非常耗时。每个人物都有大量的API调用和复杂的层次结构。我编写了一个简单的Objective-C消息拦截工具objtree,利用它记录我关注的所有消息。输出是以树状形式展现。例如,我知道某个UI事件方法会触发消息发送,因此我使用工具对该方法进行挂钩,并查看所有后续的ObjC调用,这些调用都采用了可识别栈深度的格式。下面是在触发keyDown事件时objtree随机转储的3000个选择器:

sudo objtree Messages -m "-[NSResponder keyDown:]"

接下来,在找到最重要的ObjC方法后,可以将其归纳为向某个系统进程/守护程序发送XPC消息。我为此编写了另一个工具xpcspy,它可以拦截XPC消息并启用过滤。

Xpcspy拦截到守护进程apsd的消息:

2.png

最后,我们发现守护程序apsd负责通过网络发送消息。借助Objective-C的消息分发系统,搜索其名称类似于connectTo和send的选择器,我们可以迅速找到TCP连接API调用发生的位置。

radare2搜索Objective-C选择器:

3.png

0x02 与Apple服务器通信

APN协议是一个常见的协议,简称为PUSH,目前已经有一些针对其安全性的研究成果。根据一些研究成果,该过程是通过TLS协议,使用5223端口,连接到域名rand(0,255)-courier.push.apple.com(前面为0-255的随机数字),且客户端证书被用于进行TLS层的身份验证。

但是,该协议现在已经不再通过RFC 5246中定义的CertificateRequest和Certificate消息在传输层上发送客户端证书。取而代之的是,APN在应用层将连接信息/命令与公共token、随机数和签名一起发送。

它们是在方法-[APSProtocolParser copyConnectMessageWithToken:state:presenceFlags:certificate:nonce:signature:redirectCount:lastConnected:disconnectReason:]中生成的。

其中的token参数非常重要,因为它起到了用户标识符的作用,并且在协议保护机制中起到了至关重要的作用,我们将在后面看到。

因为APN客户端证书对于每个设备都是唯一的,并且TLS加密是在应用层中进行,因此这提供了一种更安全的方法。传输层未加密,可能会将证书公开给中间人。

与Apple服务器的第一次通信发生在-[APSTCPStream _connectToServerWithPeerName:]。在该方法中,存在TLS会话配置API调用,其中包括例如-[NSURLSessionConfiguration set_socketStreamProperties:]和-[NSURLSessionConfiguration set_tlsTrustPinningPolicyName:]的私有调用。

最后,配置对象如下所示:

{
"_kCFStreamPropertyEnableConnectionStatistics" = 1;
"_kCFStreamPropertyNPNProtocolsAvailable" = (
"apns-security-v3",
"apns-pack-v1"
);
"_kCFStreamPropertyNoCompanion" = 1;
"_kCFStreamPropertyPrefersNoProxy" = 1;
"_kCFStreamSocketSetNoDelay" = 1;
kCFStreamPropertySSLSettings = {
kCFStreamSSLPeerName = "courier.push.apple.com";
kCFStreamSSLValidatesCertificateChain = 1;
};
}

现在,我们的目标是与xx-courier.push.apple.com建立开放的连接。我们尝试使用OpenSSL打开TLS连接。

% openssl s_client -connect 12-courier.push.apple.com:5223 -quiet
depth=2 O = Entrust.net, OU = www.entrust.net/CPS_2048 incorp. by ref. (limits liab.), OU = (c) 1999 Entrust.net Limited, CN = Entrust.net Certification Authority (2048)
verify return:1
depth=1 C = US, O = "Entrust, Inc.", OU = See www.entrust.net/legal-terms, OU = "(c) 2012 Entrust, Inc. - for authorized use only", CN = Entrust Certification Authority - L1K
verify return:1
depth=0 C = US, ST = California, L = Cupertino, O = Apple Inc., CN = courier.push.apple.com
verify return:1
4548513452:error:14020410:SSL routines:CONNECT_CR_SESSION_TICKET:sslv3 alert handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:1200:SSL alert number 40
4548513452:error:140200E5:SSL routines:CONNECT_CR_SESSION_TICKET:ssl handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:585:

此时握手失败。查看上面的_kCFStreamPropertyNPNProtocolsAvailable密钥,我们看到正在使用次协议协商(Next Protocol Negotiation,NPN)。

NPN现在被称作应用层协议协商(ALPN),是ClientHello消息中嵌入的TLS扩展,负责告诉TLS服务器客户端希望使用哪个应用层协议。由于这里使用了额外的TLS扩展,所以一个不错的思路是使用tcpdump记录流量并进行检查。但是首先,我们需要重新生成apsd,因为连接是在启动时发生的。Launchctl让我们能够终止,然后在调试器中生成守护程序:

% sudo launchctl attach -k system/com.apple.apsd

现在,我们apsd停止在了_dyld_start的位置:

Process 1925 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x000000010b447000 dyld _dyld_start
dyld_dyld_start:
-> 0x10b447000 : pop rdi
0x10b447001 : push 0x0
0x10b447003 : mov rbp, rsp
0x10b447006 : and rsp, -0x10
0x10b44700a : sub rsp, 0x10
0x10b44700e : mov esi, dword ptr [rbp + 0x8]
0x10b447011 : lea rdx, [rbp + 0x10]
0x10b447015 : lea rcx, [rip - 0x101c]
Target 0: (apsd) stopped.
Executable module set to "/System/Library/PrivateFrameworks/ApplePushService.framework/apsd".
Architecture set to: x86_64h-apple-macosx-.
We'll start the packet recordin, then continue apsd's execution to record the connection:
% sudo tcpdump -i en0 -w /tmp/apsd.pcap
And:
(lldb) c

在获得理想的流量转储后,我们看看握手:

4.png

在握手中,包含一些值得关注的TLS扩展。我们可以使用openssl s_client工具将大多数扩展与握手一并发送,但是在我们的尝试中,除了openssl(实际是LibreSSL 2.8.3)默认发送的消息之外,仅需要两个,也就是server_name和application_layer_protocol_negotiation扩展。对于ALPN,客户端会发送apns-security-v3和apns-pack-v1。在实际尝试中,服务器始终选择了apns-pack-v1。我们尝试使用这些参数连接到服务器:

% openssl s_client -connect 11-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet
depth=2 C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Root CA
verify error:num=19:self signed certificate in certificate chain
verify return:0

至此,我们已经成功连接到Apple的服务器。在这里,如果省略掉了-alpn或者-servername 任何一个选项,都会导致握手失败。可以忽略其中的verify error:num=19提示,这个提示是openssl针对自签名的CA证书发出的提示。

0x03 拦截APN消息

现在,我们需要拦截未加密的TLS消息。此前,证书绑定(Certificate Pinning)相对容易在APN上绕过。但由于对其进行绕过是一个完全不同的挑战,所以我选择在数据发送和接收方法上设置断点,以在明文协议Payload离开二进制之前就对其进行拦截。这里涉及到的函数分别是-[APSTCPStream writeDataInBackground:]和-[APSCourier tcpStream:dataReceived:]。

Process 1958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000109a55d83 apsd ___lldb_unnamed_symbol2607$$apsd
apsd___lldb_unnamed_symbol2607$$apsd:
-> 0x109a55d83 : push rbp
0x109a55d84 : mov rbp, rsp
0x109a55d87 : push r15
0x109a55d89 : push r14
0x109a55d8b : push rbx
0x109a55d8c : sub rsp, 0x18
0x109a55d90 : mov rbx, rdi
0x109a55d93 : mov rax, qword ptr [rip + 0x924a6] ; (void *)0x00007fff88a98af0: __stack_chk_guard
Target 0: (apsd) stopped.
(lldb) po $rdx

rdx拥有对NSData对象的引用,该对象的字节将被写入输出流。在输入流上接收数据也使用了与之相同的机制。

0x04 与APN通信

现在,我们有了连接和数据,就可以尝试通过APN进行通信了。我们来测试一下,这里我使用FIFO将输入传到openssl。

% mkfifo /tmp/in
% openssl s_client -connect 12-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet < /tmp/in > /tmp/out
And for reading response messages enter:
% xxd /tmp/out
00000000: 0822 0180 04a1 1400 0588 0683 08a9 3800 ."............8.
00000010: 0aa5 0176 1474 ee7b 0ca5 0176 1474 ee7b ...v.t.{...v.t.{

此时,连接响应消息中包含响应代码(0x08)、服务器时间以及其他参数。

0x05 断开连接

APN是一种二进制协议。这些命令已经在APSProtocolParser类中进行了序列化,其内部并不是我们想要的。根据我们对apsd内部进行的分析,以下是能够发送iMessage所需的最小化命令序列:

0x07:使用uid 0连接用户(每个用户都有自己的公共push token);

0x0c:保持连接(Keep Alive);

0x14:活动状态;

0x07:使用uid 501连接用户;

0x09:过滤主题;

0x0a:发送消息。

通过复制apsd发送时的二进制消息数据,并将其作为我们的openssl FIFO设置的输入,我们可以完全从openssl复现iMessage的发送。在这些命令中,最值得关注的是filter(0x09)。filter消息在方法-[APSProtocolParser copyFilterMessageWithEnabledHashes:ignoredHashes:opportunisticHashes:nonWakingHashes:pausedHashes:token:]中被序列化。参数中的哈希值用于主题或使用APN的服务。出于某种原因,iMessage的主题名称为com.apple.madrid。如果没有过滤器消息,客户端就无法通过sendcommand(0x0a)发送或接收APN消息。因此,我们必须在发送消息之前调用filter命令。

0x06 总结

如我们所见,复现APN流量非常容易,但需要注意的一点是,filter命令将导致服务器删除同一公共token的所有先前连接。

假设我们已经经历了逆向协议的痛苦,从头开始生成APN有效消息,然后构建了一个名为fakeapsd的Linux APN客户端,并从Mac设备上原样复制了连接消息参数(公共token和密钥对)。如上所述,使用filter命令进行连接断开的含义是,每次fakeapsd尝试与服务器进行任何有意义的通信时,都会导致真正的apsd连接断开,进而尝试重新连接,并且fakeapsd永远会和apsd抢夺连接。

既然服务器会针对同一个公共token断开连接,而这是连接消息的关键参数,那么,是否可以生成一个新的来绕过这一限制呢?无需花费宝贵的时间来进行研究,我们可以向Hackintosh(黑苹果)寻求答案。

我们从技术角度上来分析Hackintosh,这其实是一个完美的实验,因为在其他所有条件(包括协议处理等等)相同的情况下,它能够控制一个重要的参数,使系统误认为它运行在一台真正的Apple设备上。长期以来,在非苹果设备上使用iMessage和FaceTime一直存在着很多问题,因为必须首先暴力破解出序列号,直到寻找到一个真实、尚未购买的序列号为止。

我们知道,在攻击者可以完整访问软件的白盒场景中,控制硬件可能是用于“保护”协议的一个最重要的元素。类似于Frida这样的动态分析工具可以加速逆向工程的进度。

要了解有关移动应用程序安全性和更多研究成果,欢迎与我们的专家进行沟通。

0x00 概述

iMessage是整个Apple生态系统中广泛使用的安全消息传递应用程序和协议。由于对在其他平台上运行的iMessage原理感到好奇,我们使用了一种逆向工程的方式来探究iMessage的运行方式,并研究将其扩展到其他平台的可能性。

本文的目标是说明Apple如何利用其硬件产品来保护其软件。为了对此进行探讨,我们将尝试通过Apple Push Notification(APN)直接在网络级别进行连接,并解决遇到的问题。在这个过程中,我们将使用流行的开源工具对macOS上apsd守护程序的一部分以及APN协议自身进行逆向工程。

0x01 当前解决方案

在当前,如果要在Apple生态系统之外运行iMessage,就必须使用Mac服务器,并依靠AppleScript脚本来自动执行Messages.app UI操作。这样一来,就无需再客户端上重新实现消息发送协议。但是,这里最大的问题在于,如果要使用iMessage,则Mac必须一直运行。

与针对独立二进制文件进行逆向有所不同,iMessage发送代码(像XNU OS中大多数内部函数一样)已经超出了Messages.app的范围,并且该进程依赖于许多系统守护程序,即微服务体系结构,并且它们依赖XPC消息作为IPC(进程间通信)机制。

Project Zero已经对iMessage中涉及的守护程序结构进行了充分的研究,因此在这篇文章中我们将省略不必要的细节。简而言之,如果我们编辑好一条消息,并按下回车键,就会经历Messages.app -> imagent -> identityservicesd -> apsd的过程。为了进一步对这个过程进行分析,我编写了两个基于Frida的工具,并在此过程中遇到了两大挑战。

首先,在反汇编程序中静态查找ObjC方法非常耗时。每个人物都有大量的API调用和复杂的层次结构。我编写了一个简单的Objective-C消息拦截工具objtree,利用它记录我关注的所有消息。输出是以树状形式展现。例如,我知道某个UI事件方法会触发消息发送,因此我使用工具对该方法进行挂钩,并查看所有后续的ObjC调用,这些调用都采用了可识别栈深度的格式。下面是在触发keyDown事件时objtree随机转储的3000个选择器:

sudo objtree Messages -m "-[NSResponder keyDown:]"

接下来,在找到最重要的ObjC方法后,可以将其归纳为向某个系统进程/守护程序发送XPC消息。我为此编写了另一个工具xpcspy,它可以拦截XPC消息并启用过滤。

Xpcspy拦截到守护进程apsd的消息:

2.png

最后,我们发现守护程序apsd负责通过网络发送消息。借助Objective-C的消息分发系统,搜索其名称类似于connectTo和send的选择器,我们可以迅速找到TCP连接API调用发生的位置。

radare2搜索Objective-C选择器:

3.png

0x02 与Apple服务器通信

APN协议是一个常见的协议,简称为PUSH,目前已经有一些针对其安全性的研究成果。根据一些研究成果,该过程是通过TLS协议,使用5223端口,连接到域名rand(0,255)-courier.push.apple.com(前面为0-255的随机数字),且客户端证书被用于进行TLS层的身份验证。

但是,该协议现在已经不再通过RFC 5246中定义的CertificateRequest和Certificate消息在传输层上发送客户端证书。取而代之的是,APN在应用层将连接信息/命令与公共token、随机数和签名一起发送。

它们是在方法-[APSProtocolParser copyConnectMessageWithToken:state:presenceFlags:certificate:nonce:signature:redirectCount:lastConnected:disconnectReason:]中生成的。

其中的token参数非常重要,因为它起到了用户标识符的作用,并且在协议保护机制中起到了至关重要的作用,我们将在后面看到。

因为APN客户端证书对于每个设备都是唯一的,并且TLS加密是在应用层中进行,因此这提供了一种更安全的方法。传输层未加密,可能会将证书公开给中间人。

与Apple服务器的第一次通信发生在-[APSTCPStream _connectToServerWithPeerName:]。在该方法中,存在TLS会话配置API调用,其中包括例如-[NSURLSessionConfiguration set_socketStreamProperties:]和-[NSURLSessionConfiguration set_tlsTrustPinningPolicyName:]的私有调用。

最后,配置对象如下所示:

{
"_kCFStreamPropertyEnableConnectionStatistics" = 1;
"_kCFStreamPropertyNPNProtocolsAvailable" = (
"apns-security-v3",
"apns-pack-v1"
);
"_kCFStreamPropertyNoCompanion" = 1;
"_kCFStreamPropertyPrefersNoProxy" = 1;
"_kCFStreamSocketSetNoDelay" = 1;
kCFStreamPropertySSLSettings = {
kCFStreamSSLPeerName = "courier.push.apple.com";
kCFStreamSSLValidatesCertificateChain = 1;
};
}

现在,我们的目标是与xx-courier.push.apple.com建立开放的连接。我们尝试使用OpenSSL打开TLS连接。

% openssl s_client -connect 12-courier.push.apple.com:5223 -quiet
depth=2 O = Entrust.net, OU = www.entrust.net/CPS_2048 incorp. by ref. (limits liab.), OU = (c) 1999 Entrust.net Limited, CN = Entrust.net Certification Authority (2048)
verify return:1
depth=1 C = US, O = "Entrust, Inc.", OU = See www.entrust.net/legal-terms, OU = "(c) 2012 Entrust, Inc. - for authorized use only", CN = Entrust Certification Authority - L1K
verify return:1
depth=0 C = US, ST = California, L = Cupertino, O = Apple Inc., CN = courier.push.apple.com
verify return:1
4548513452:error:14020410:SSL routines:CONNECT_CR_SESSION_TICKET:sslv3 alert handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:1200:SSL alert number 40
4548513452:error:140200E5:SSL routines:CONNECT_CR_SESSION_TICKET:ssl handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:585:

此时握手失败。查看上面的_kCFStreamPropertyNPNProtocolsAvailable密钥,我们看到正在使用次协议协商(Next Protocol Negotiation,NPN)。

NPN现在被称作应用层协议协商(ALPN),是ClientHello消息中嵌入的TLS扩展,负责告诉TLS服务器客户端希望使用哪个应用层协议。由于这里使用了额外的TLS扩展,所以一个不错的思路是使用tcpdump记录流量并进行检查。但是首先,我们需要重新生成apsd,因为连接是在启动时发生的。Launchctl让我们能够终止,然后在调试器中生成守护程序:

% sudo launchctl attach -k system/com.apple.apsd

现在,我们apsd停止在了_dyld_start的位置:

Process 1925 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x000000010b447000 dyld _dyld_start
dyld_dyld_start:
-> 0x10b447000 : pop rdi
0x10b447001 : push 0x0
0x10b447003 : mov rbp, rsp
0x10b447006 : and rsp, -0x10
0x10b44700a : sub rsp, 0x10
0x10b44700e : mov esi, dword ptr [rbp + 0x8]
0x10b447011 : lea rdx, [rbp + 0x10]
0x10b447015 : lea rcx, [rip - 0x101c]
Target 0: (apsd) stopped.
Executable module set to "/System/Library/PrivateFrameworks/ApplePushService.framework/apsd".
Architecture set to: x86_64h-apple-macosx-.
We'll start the packet recordin, then continue apsd's execution to record the connection:
% sudo tcpdump -i en0 -w /tmp/apsd.pcap
And:
(lldb) c

在获得理想的流量转储后,我们看看握手:

4.png

在握手中,包含一些值得关注的TLS扩展。我们可以使用openssl s_client工具将大多数扩展与握手一并发送,但是在我们的尝试中,除了openssl(实际是LibreSSL 2.8.3)默认发送的消息之外,仅需要两个,也就是server_name和application_layer_protocol_negotiation扩展。对于ALPN,客户端会发送apns-security-v3和apns-pack-v1。在实际尝试中,服务器始终选择了apns-pack-v1。我们尝试使用这些参数连接到服务器:

% openssl s_client -connect 11-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet
depth=2 C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Root CA
verify error:num=19:self signed certificate in certificate chain
verify return:0

至此,我们已经成功连接到Apple的服务器。在这里,如果省略掉了-alpn或者-servername 任何一个选项,都会导致握手失败。可以忽略其中的verify error:num=19提示,这个提示是openssl针对自签名的CA证书发出的提示。

0x03 拦截APN消息

现在,我们需要拦截未加密的TLS消息。此前,证书绑定(Certificate Pinning)相对容易在APN上绕过。但由于对其进行绕过是一个完全不同的挑战,所以我选择在数据发送和接收方法上设置断点,以在明文协议Payload离开二进制之前就对其进行拦截。这里涉及到的函数分别是-[APSTCPStream writeDataInBackground:]和-[APSCourier tcpStream:dataReceived:]。

Process 1958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000109a55d83 apsd ___lldb_unnamed_symbol2607$$apsd
apsd___lldb_unnamed_symbol2607$$apsd:
-> 0x109a55d83 : push rbp
0x109a55d84 : mov rbp, rsp
0x109a55d87 : push r15
0x109a55d89 : push r14
0x109a55d8b : push rbx
0x109a55d8c : sub rsp, 0x18
0x109a55d90 : mov rbx, rdi
0x109a55d93 : mov rax, qword ptr [rip + 0x924a6] ; (void *)0x00007fff88a98af0: __stack_chk_guard
Target 0: (apsd) stopped.
(lldb) po $rdx

rdx拥有对NSData对象的引用,该对象的字节将被写入输出流。在输入流上接收数据也使用了与之相同的机制。

0x04 与APN通信

现在,我们有了连接和数据,就可以尝试通过APN进行通信了。我们来测试一下,这里我使用FIFO将输入传到openssl。

% mkfifo /tmp/in
% openssl s_client -connect 12-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet < /tmp/in > /tmp/out
And for reading response messages enter:
% xxd /tmp/out
00000000: 0822 0180 04a1 1400 0588 0683 08a9 3800 ."............8.
00000010: 0aa5 0176 1474 ee7b 0ca5 0176 1474 ee7b ...v.t.{...v.t.{

此时,连接响应消息中包含响应代码(0x08)、服务器时间以及其他参数。

0x05 断开连接

APN是一种二进制协议。这些命令已经在APSProtocolParser类中进行了序列化,其内部并不是我们想要的。根据我们对apsd内部进行的分析,以下是能够发送iMessage所需的最小化命令序列:

0x07:使用uid 0连接用户(每个用户都有自己的公共push token);

0x0c:保持连接(Keep Alive);

0x14:活动状态;

0x07:使用uid 501连接用户;

0x09:过滤主题;

0x0a:发送消息。

通过复制apsd发送时的二进制消息数据,并将其作为我们的openssl FIFO设置的输入,我们可以完全从openssl复现iMessage的发送。在这些命令中,最值得关注的是filter(0x09)。filter消息在方法-[APSProtocolParser copyFilterMessageWithEnabledHashes:ignoredHashes:opportunisticHashes:nonWakingHashes:pausedHashes:token:]中被序列化。参数中的哈希值用于主题或使用APN的服务。出于某种原因,iMessage的主题名称为com.apple.madrid。如果没有过滤器消息,客户端就无法通过sendcommand(0x0a)发送或接收APN消息。因此,我们必须在发送消息之前调用filter命令。

0x06 总结

如我们所见,复现APN流量非常容易,但需要注意的一点是,filter命令将导致服务器删除同一公共token的所有先前连接。

假设我们已经经历了逆向协议的痛苦,从头开始生成APN有效消息,然后构建了一个名为fakeapsd的Linux APN客户端,并从Mac设备上原样复制了连接消息参数(公共token和密钥对)。如上所述,使用filter命令进行连接断开的含义是,每次fakeapsd尝试与服务器进行任何有意义的通信时,都会导致真正的apsd连接断开,进而尝试重新连接,并且fakeapsd永远会和apsd抢夺连接。

既然服务器会针对同一个公共token断开连接,而这是连接消息的关键参数,那么,是否可以生成一个新的来绕过这一限制呢?无需花费宝贵的时间来进行研究,我们可以向Hackintosh(黑苹果)寻求答案。

我们从技术角度上来分析Hackintosh,这其实是一个完美的实验,因为在其他所有条件(包括协议处理等等)相同的情况下,它能够控制一个重要的参数,使系统误认为它运行在一台真正的Apple设备上。长期以来,在非苹果设备上使用iMessage和FaceTime一直存在着很多问题,因为必须首先暴力破解出序列号,直到寻找到一个真实、尚未购买的序列号为止。

我们知道,在攻击者可以完整访问软件的白盒场景中,控制硬件可能是用于“保护”协议的一个最重要的元素。类似于Frida这样的动态分析工具可以加速逆向工程的进度。

要了解有关移动应用程序安全性和更多研究成果,欢迎与我们的专家进行沟通。

0x00 概述

iMessage是整个Apple生态系统中广泛使用的安全消息传递应用程序和协议。由于对在其他平台上运行的iMessage原理感到好奇,我们使用了一种逆向工程的方式来探究iMessage的运行方式,并研究将其扩展到其他平台的可能性。

本文的目标是说明Apple如何利用其硬件产品来保护其软件。为了对此进行探讨,我们将尝试通过Apple Push Notification(APN)直接在网络级别进行连接,并解决遇到的问题。在这个过程中,我们将使用流行的开源工具对macOS上apsd守护程序的一部分以及APN协议自身进行逆向工程。

0x01 当前解决方案

在当前,如果要在Apple生态系统之外运行iMessage,就必须使用Mac服务器,并依靠AppleScript脚本来自动执行Messages.app UI操作。这样一来,就无需再客户端上重新实现消息发送协议。但是,这里最大的问题在于,如果要使用iMessage,则Mac必须一直运行。

与针对独立二进制文件进行逆向有所不同,iMessage发送代码(像XNU OS中大多数内部函数一样)已经超出了Messages.app的范围,并且该进程依赖于许多系统守护程序,即微服务体系结构,并且它们依赖XPC消息作为IPC(进程间通信)机制。

Project Zero已经对iMessage中涉及的守护程序结构进行了充分的研究,因此在这篇文章中我们将省略不必要的细节。简而言之,如果我们编辑好一条消息,并按下回车键,就会经历Messages.app -> imagent -> identityservicesd -> apsd的过程。为了进一步对这个过程进行分析,我编写了两个基于Frida的工具,并在此过程中遇到了两大挑战。

首先,在反汇编程序中静态查找ObjC方法非常耗时。每个人物都有大量的API调用和复杂的层次结构。我编写了一个简单的Objective-C消息拦截工具objtree,利用它记录我关注的所有消息。输出是以树状形式展现。例如,我知道某个UI事件方法会触发消息发送,因此我使用工具对该方法进行挂钩,并查看所有后续的ObjC调用,这些调用都采用了可识别栈深度的格式。下面是在触发keyDown事件时objtree随机转储的3000个选择器:

sudo objtree Messages -m "-[NSResponder keyDown:]"

接下来,在找到最重要的ObjC方法后,可以将其归纳为向某个系统进程/守护程序发送XPC消息。我为此编写了另一个工具xpcspy,它可以拦截XPC消息并启用过滤。

Xpcspy拦截到守护进程apsd的消息:

2.png

最后,我们发现守护程序apsd负责通过网络发送消息。借助Objective-C的消息分发系统,搜索其名称类似于connectTo和send的选择器,我们可以迅速找到TCP连接API调用发生的位置。

radare2搜索Objective-C选择器:

3.png

0x02 与Apple服务器通信

APN协议是一个常见的协议,简称为PUSH,目前已经有一些针对其安全性的研究成果。根据一些研究成果,该过程是通过TLS协议,使用5223端口,连接到域名rand(0,255)-courier.push.apple.com(前面为0-255的随机数字),且客户端证书被用于进行TLS层的身份验证。

但是,该协议现在已经不再通过RFC 5246中定义的CertificateRequest和Certificate消息在传输层上发送客户端证书。取而代之的是,APN在应用层将连接信息/命令与公共token、随机数和签名一起发送。

它们是在方法-[APSProtocolParser copyConnectMessageWithToken:state:presenceFlags:certificate:nonce:signature:redirectCount:lastConnected:disconnectReason:]中生成的。

其中的token参数非常重要,因为它起到了用户标识符的作用,并且在协议保护机制中起到了至关重要的作用,我们将在后面看到。

因为APN客户端证书对于每个设备都是唯一的,并且TLS加密是在应用层中进行,因此这提供了一种更安全的方法。传输层未加密,可能会将证书公开给中间人。

与Apple服务器的第一次通信发生在-[APSTCPStream _connectToServerWithPeerName:]。在该方法中,存在TLS会话配置API调用,其中包括例如-[NSURLSessionConfiguration set_socketStreamProperties:]和-[NSURLSessionConfiguration set_tlsTrustPinningPolicyName:]的私有调用。

最后,配置对象如下所示:

{
"_kCFStreamPropertyEnableConnectionStatistics" = 1;
"_kCFStreamPropertyNPNProtocolsAvailable" = (
"apns-security-v3",
"apns-pack-v1"
);
"_kCFStreamPropertyNoCompanion" = 1;
"_kCFStreamPropertyPrefersNoProxy" = 1;
"_kCFStreamSocketSetNoDelay" = 1;
kCFStreamPropertySSLSettings = {
kCFStreamSSLPeerName = "courier.push.apple.com";
kCFStreamSSLValidatesCertificateChain = 1;
};
}

现在,我们的目标是与xx-courier.push.apple.com建立开放的连接。我们尝试使用OpenSSL打开TLS连接。

% openssl s_client -connect 12-courier.push.apple.com:5223 -quiet
depth=2 O = Entrust.net, OU = www.entrust.net/CPS_2048 incorp. by ref. (limits liab.), OU = (c) 1999 Entrust.net Limited, CN = Entrust.net Certification Authority (2048)
verify return:1
depth=1 C = US, O = "Entrust, Inc.", OU = See www.entrust.net/legal-terms, OU = "(c) 2012 Entrust, Inc. - for authorized use only", CN = Entrust Certification Authority - L1K
verify return:1
depth=0 C = US, ST = California, L = Cupertino, O = Apple Inc., CN = courier.push.apple.com
verify return:1
4548513452:error:14020410:SSL routines:CONNECT_CR_SESSION_TICKET:sslv3 alert handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:1200:SSL alert number 40
4548513452:error:140200E5:SSL routines:CONNECT_CR_SESSION_TICKET:ssl handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:585:

此时握手失败。查看上面的_kCFStreamPropertyNPNProtocolsAvailable密钥,我们看到正在使用次协议协商(Next Protocol Negotiation,NPN)。

NPN现在被称作应用层协议协商(ALPN),是ClientHello消息中嵌入的TLS扩展,负责告诉TLS服务器客户端希望使用哪个应用层协议。由于这里使用了额外的TLS扩展,所以一个不错的思路是使用tcpdump记录流量并进行检查。但是首先,我们需要重新生成apsd,因为连接是在启动时发生的。Launchctl让我们能够终止,然后在调试器中生成守护程序:

% sudo launchctl attach -k system/com.apple.apsd

现在,我们apsd停止在了_dyld_start的位置:

Process 1925 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x000000010b447000 dyld _dyld_start
dyld_dyld_start:
-> 0x10b447000 : pop rdi
0x10b447001 : push 0x0
0x10b447003 : mov rbp, rsp
0x10b447006 : and rsp, -0x10
0x10b44700a : sub rsp, 0x10
0x10b44700e : mov esi, dword ptr [rbp + 0x8]
0x10b447011 : lea rdx, [rbp + 0x10]
0x10b447015 : lea rcx, [rip - 0x101c]
Target 0: (apsd) stopped.
Executable module set to "/System/Library/PrivateFrameworks/ApplePushService.framework/apsd".
Architecture set to: x86_64h-apple-macosx-.
We'll start the packet recordin, then continue apsd's execution to record the connection:
% sudo tcpdump -i en0 -w /tmp/apsd.pcap
And:
(lldb) c

在获得理想的流量转储后,我们看看握手:

4.png

在握手中,包含一些值得关注的TLS扩展。我们可以使用openssl s_client工具将大多数扩展与握手一并发送,但是在我们的尝试中,除了openssl(实际是LibreSSL 2.8.3)默认发送的消息之外,仅需要两个,也就是server_name和application_layer_protocol_negotiation扩展。对于ALPN,客户端会发送apns-security-v3和apns-pack-v1。在实际尝试中,服务器始终选择了apns-pack-v1。我们尝试使用这些参数连接到服务器:

% openssl s_client -connect 11-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet
depth=2 C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Root CA
verify error:num=19:self signed certificate in certificate chain
verify return:0

至此,我们已经成功连接到Apple的服务器。在这里,如果省略掉了-alpn或者-servername 任何一个选项,都会导致握手失败。可以忽略其中的verify error:num=19提示,这个提示是openssl针对自签名的CA证书发出的提示。

0x03 拦截APN消息

现在,我们需要拦截未加密的TLS消息。此前,证书绑定(Certificate Pinning)相对容易在APN上绕过。但由于对其进行绕过是一个完全不同的挑战,所以我选择在数据发送和接收方法上设置断点,以在明文协议Payload离开二进制之前就对其进行拦截。这里涉及到的函数分别是-[APSTCPStream writeDataInBackground:]和-[APSCourier tcpStream:dataReceived:]。

Process 1958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000109a55d83 apsd ___lldb_unnamed_symbol2607$$apsd
apsd___lldb_unnamed_symbol2607$$apsd:
-> 0x109a55d83 : push rbp
0x109a55d84 : mov rbp, rsp
0x109a55d87 : push r15
0x109a55d89 : push r14
0x109a55d8b : push rbx
0x109a55d8c : sub rsp, 0x18
0x109a55d90 : mov rbx, rdi
0x109a55d93 : mov rax, qword ptr [rip + 0x924a6] ; (void *)0x00007fff88a98af0: __stack_chk_guard
Target 0: (apsd) stopped.
(lldb) po $rdx

rdx拥有对NSData对象的引用,该对象的字节将被写入输出流。在输入流上接收数据也使用了与之相同的机制。

0x04 与APN通信

现在,我们有了连接和数据,就可以尝试通过APN进行通信了。我们来测试一下,这里我使用FIFO将输入传到openssl。

% mkfifo /tmp/in
% openssl s_client -connect 12-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet < /tmp/in > /tmp/out
And for reading response messages enter:
% xxd /tmp/out
00000000: 0822 0180 04a1 1400 0588 0683 08a9 3800 ."............8.
00000010: 0aa5 0176 1474 ee7b 0ca5 0176 1474 ee7b ...v.t.{...v.t.{

此时,连接响应消息中包含响应代码(0x08)、服务器时间以及其他参数。

0x05 断开连接

APN是一种二进制协议。这些命令已经在APSProtocolParser类中进行了序列化,其内部并不是我们想要的。根据我们对apsd内部进行的分析,以下是能够发送iMessage所需的最小化命令序列:

0x07:使用uid 0连接用户(每个用户都有自己的公共push token);

0x0c:保持连接(Keep Alive);

0x14:活动状态;

0x07:使用uid 501连接用户;

0x09:过滤主题;

0x0a:发送消息。

通过复制apsd发送时的二进制消息数据,并将其作为我们的openssl FIFO设置的输入,我们可以完全从openssl复现iMessage的发送。在这些命令中,最值得关注的是filter(0x09)。filter消息在方法-[APSProtocolParser copyFilterMessageWithEnabledHashes:ignoredHashes:opportunisticHashes:nonWakingHashes:pausedHashes:token:]中被序列化。参数中的哈希值用于主题或使用APN的服务。出于某种原因,iMessage的主题名称为com.apple.madrid。如果没有过滤器消息,客户端就无法通过sendcommand(0x0a)发送或接收APN消息。因此,我们必须在发送消息之前调用filter命令。

0x06 总结

如我们所见,复现APN流量非常容易,但需要注意的一点是,filter命令将导致服务器删除同一公共token的所有先前连接。

假设我们已经经历了逆向协议的痛苦,从头开始生成APN有效消息,然后构建了一个名为fakeapsd的Linux APN客户端,并从Mac设备上原样复制了连接消息参数(公共token和密钥对)。如上所述,使用filter命令进行连接断开的含义是,每次fakeapsd尝试与服务器进行任何有意义的通信时,都会导致真正的apsd连接断开,进而尝试重新连接,并且fakeapsd永远会和apsd抢夺连接。

既然服务器会针对同一个公共token断开连接,而这是连接消息的关键参数,那么,是否可以生成一个新的来绕过这一限制呢?无需花费宝贵的时间来进行研究,我们可以向Hackintosh(黑苹果)寻求答案。

我们从技术角度上来分析Hackintosh,这其实是一个完美的实验,因为在其他所有条件(包括协议处理等等)相同的情况下,它能够控制一个重要的参数,使系统误认为它运行在一台真正的Apple设备上。长期以来,在非苹果设备上使用iMessage和FaceTime一直存在着很多问题,因为必须首先暴力破解出序列号,直到寻找到一个真实、尚未购买的序列号为止。

我们知道,在攻击者可以完整访问软件的白盒场景中,控制硬件可能是用于“保护”协议的一个最重要的元素。类似于Frida这样的动态分析工具可以加速逆向工程的进度。

要了解有关移动应用程序安全性和更多研究成果,欢迎与我们的专家进行沟通。

0x00 概述

在macOS 10.15.2版本上,Apple引入了com.apple.private.security.clear-library-validation权限(entitlement),该权限正在逐渐取代以前在系统二进制文件上使用的com.apple.security.cs.disable-library-validation权限。尽管二者的影响大致相同,但它们的工作原理却存在差异。尽管使用com.apple.security.cs.disable-library-validation和com.apple.private.security.clear-library-validation会自动禁用库验证,但应用程序必须通过csops系统调用将其禁用。

0x01 简介

在Big Sur版本发布后,我注意到许多系统二进制文件都具有新的权限,其中的com.apple.private.security.clear-library-validation是我此前没有接触过的。这些应用程序之前使用的是com.apple.security.cs.disable-library-validation,看来它们似乎已经被一个新的权限替换。由于二者的名字比较相似,并且经过测试也证实了这些二进制文件仍然可以加载非Apple开发人员签名的第三方插件。这意味着,这些权限具有相同的影响。但是,二者的内部工作原理是不同的。

0x02 csops系统调用

在遇到下面列出的新csops操作代码后,我开始深入研究这一新的权限,可以在xnu-7195.50.7.100.1/bsd/sys/codesign.h中找到该代码。

#define CS_OPS_CLEAR_LV     15  /* clear the library validation flag */

csops是一个系统调用,可以用于对进程执行各类与代码签名相关的操作。我们可以查询进程的状态,在运行时设置各种标志,查询其代码签名blob等等。这是我以前没有发现过的新功能,因此我开始对其进行分析。

根据这个常量的说明,我们可以使用这个操作代码来清除进程的库验证标志。这意味着,如果我们可以在某个进程上运行它,则在调用成功的情况下,可以将第三方库加载到该进程中。

这个常量仅在xnu-7195.50.7.100.1/bsd/kern/kern_proc.c源文件中引用,该文件中包含csops_internal函数的源代码。这是在进行系统调用时将会运行的函数。下面是与CS_OPS_CLEAR_LV操作相关的部分源代码。

static int
csops_internal(pid_t pid, int ops, user_addr_t uaddr, user_size_t usersize, user_addr_t uaudittoken)
{
(...)
       if (pid == 0) {
              pid = proc_selfpid();
       }
       if (pid == proc_selfpid()) {
              forself = 1;
       }
 
 
       switch (ops) {
       case CS_OPS_STATUS:
       case CS_OPS_CDHASH:
       case CS_OPS_PIDOFFSET:
       case CS_OPS_ENTITLEMENTS_BLOB:
       case CS_OPS_IDENTITY:
       case CS_OPS_BLOB:
       case CS_OPS_TEAMID:
       case CS_OPS_CLEAR_LV:
              break;          /* not restricted to root */
       default:
              if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
                     return EPERM;
              }
              break;
       }
 
       pt = proc_find(pid);
       if (pt == PROC_NULL) {
              return ESRCH;
       }
(...)
#if CONFIG_MACF
       switch (ops) {
       case CS_OPS_MARKINVALID:
       case CS_OPS_MARKHARD:
       case CS_OPS_MARKKILL:
       case CS_OPS_MARKRESTRICT:
       case CS_OPS_SET_STATUS:
       case CS_OPS_CLEARINSTALLER:
       case CS_OPS_CLEARPLATFORM:
       case CS_OPS_CLEAR_LV:
              if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
              break;
       default:
              if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
       }
#endif
       switch (ops) {
(...)
       case CS_OPS_CLEAR_LV: {
              /*
               * This option is used to remove library validation from
               * a running process. This is used in plugin architectures
               * when a program needs to load untrusted libraries. This
               * allows the process to maintain library validation as
               * long as possible, then drop it only when required.
               * Once a process has loaded the untrusted library,
               * relying on library validation in the future will
               * not be effective. An alternative is to re-exec
               * your application without library validation, or
               * fork an untrusted child.
               */
#if !defined(XNU_TARGET_OS_OSX)
              // We only support dropping library validation on macOS
              error = ENOTSUP;
#else
              /*
               * if we have the flag set, and the caller wants
               * to remove it, and they're entitled to, then
               * we remove it from the csflags
               *
               * NOTE: We are fine to poke into the task because
               * we get a ref to pt when we do the proc_find
               * at the beginning of this function.
               *
               * We also only allow altering ourselves.
               */
              if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
                     proc_lock(pt);
                     pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
                     proc_unlock(pt);
                     error = 0;
              } else {
                     error = EPERM;
              }
(...)
}

我们将一步一步进行介绍。其中,有三个地方会对其进行检查。我们首先来看第一个swtich条件语句。

switch (ops) {
       case CS_OPS_STATUS:
       case CS_OPS_CDHASH:
       case CS_OPS_PIDOFFSET:
       case CS_OPS_ENTITLEMENTS_BLOB:
       case CS_OPS_IDENTITY:
       case CS_OPS_BLOB:
       case CS_OPS_TEAMID:
       case CS_OPS_CLEAR_LV:
              break;          /* not restricted to root */
       default:
              if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
                     return EPERM;
              }
              break;
       }

在这里,系统将允许非root执行switch条件中列出的操作,其中的一项是我们关注的重点。这表明,即使我们没有以root用户身份运行,也可以使用CS_OPS_CLEAR_LV操作调用csops。

接下来,我们来分析另一个switch条件。

#if CONFIG_MACF
       switch (ops) {
       case CS_OPS_MARKINVALID:
       case CS_OPS_MARKHARD:
       case CS_OPS_MARKKILL:
       case CS_OPS_MARKRESTRICT:
       case CS_OPS_SET_STATUS:
       case CS_OPS_CLEARINSTALLER:
       case CS_OPS_CLEARPLATFORM:
       case CS_OPS_CLEAR_LV:
              if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
              break;
       default:
              if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
       }
#endif

在这里,我们使用mac_proc_check_get_cs_info函数进行了MACF策略调用。如果成功,MACF策略调用将返回0,这就是对条件的检查。在xnu-7195.50.7.100.1/security/mac_process.c内部实现了mac_proc_check_get_cs_info函数。我们跟踪这个函数。

int
mac_proc_check_set_cs_info(proc_t curp, proc_t target, unsigned int op)
{
       kauth_cred_t cred;
       int error = 0;
 
#if SECURITY_MAC_CHECK_ENFORCE
       /* 21167099 - only check if we allow write */
       if (!mac_proc_enforce) {
              return 0;
       }
#endif
       if (!mac_proc_check_enforce(curp)) {
              return 0;
       }
 
       cred = kauth_cred_proc_ref(curp);
       MAC_CHECK(proc_check_set_cs_info, cred, target, op);
       kauth_cred_unref(&cred);
 
       return error;
}

该函数最终将使用MAC_CHECK宏进行MACF调用,我在针对CVE-2020-9771补丁的逆向分析过程中已经讨论过。它将遍历MACF策略挂钩,该挂钩对proc_check_set_cs_info进行检查。目前,它仅仅被沙箱挂钩,如下所示。

void _hook_proc_check_set_cs_info(int arg0, int arg1) {
    ___bzero(&var_1A0, 0x188);
    *(int32_t *)(&var_1A0 + 0xa8) = 0x4;
    *(&var_1A0 + 0xb8) = arg1;
    _cred_sb_evaluate(arg0, 0x65, &var_1A0, rcx, r8, r9);
    return;
}

在这里,将使用操作码0x65对_cred_sb_evaluate进行内部调用,我在上一篇文章中也对此进行了讨论。

回到我们的csops调用中,不论调用过程允许或不允许这个操作,都会运行MACF策略检查。

假设允许这个操作,那么我们继续,最后到达实际执行该操作的位置。

       case CS_OPS_CLEAR_LV: {
              /*
               * This option is used to remove library validation from
               * a running process. This is used in plugin architectures
               * when a program needs to load untrusted libraries. This
               * allows the process to maintain library validation as
               * long as possible, then drop it only when required.
               * Once a process has loaded the untrusted library,
               * relying on library validation in the future will
               * not be effective. An alternative is to re-exec
               * your application without library validation, or
               * fork an untrusted child.
               */
#if !defined(XNU_TARGET_OS_OSX)
              // We only support dropping library validation on macOS
              error = ENOTSUP;
#else
              /*
               * if we have the flag set, and the caller wants
               * to remove it, and they're entitled to, then
               * we remove it from the csflags
               *
               * NOTE: We are fine to poke into the task because
               * we get a ref to pt when we do the proc_find
               * at the beginning of this function.
               *
               * We also only allow altering ourselves.
               */
              if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
                     proc_lock(pt);
                     pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
                     proc_unlock(pt);
                     error = 0;
              } else {
                     error = EPERM;
              }

在这里,Apple添加了非常详细的注释,对所有内容都进行了说明。如果满足要求,它将清除目标进程的库验证标志(pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));)。

条件非常严格。该操作只能由进程自身(forself == 1)以及具有由常量CLEAR_LV_ENTITLEMENT定义的权限的进程调用。这是在xnu-7195.50.7.100.1/bsd/sys/codesign.h中进行的定义。

#define CLEAR_LV_ENTITLEMENT "com.apple.private.security.clear-library-validation"

在循环结束后,我们就得到了以前曾见到过的权限。

综上所述,我们可以确定,拥有com.apple.private.security.clear-library-validation权限的进程可以使用CLEAR_LV_ENTITLEMENT调用csops系统调用,以清除自身的库验证代码签名标志。即使该进程未以root用户身份运行,这个方法也是有效的。

0x03 SSH

为了确认这个发现,我将具有这个权限的SSH加载到Hopper中,并验证它是否使用csops禁用了库验证。

int sub_10016d41f(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
(...)
loc_10016d67b:
    rax = getpid();
    rax = csops(rax, 0xf, 0x0, 0x0);
    if (rax == 0x0) goto loc_10016d6d6;
 
loc_10016d694:
    rdx = 0x0;
    rcx = 0x0;
    rbx = 0x0;
    sub_10014e556("csops(CS_OPS_CLEAR_LV) failed: %d", rax, rdx, rcx, r8, r9, stack[-136]);
(...)

的确,我们可以找到使用操作码0xf调用csops的函数,也就是CS_OPS_CLEAR_LV操作。如果该调用失败,我们还能得到一个详细的错误消息。

0x04 历史

尽管我仅仅在Big Sur版本中发现了这个变化,但是这个功能是较早之前引入的。在xnu-6153.51.1中引入了用于清除库验证标志的详细csops操作,该操作已经在macOS 10.15.2中使用。但是使用该权限的二进制文件仅在macOS 10.15.4版本以后出现,只涉及四个应用程序——su、screen、login和passwd。

从Big Sur(macOS 11.0)开始,有20个二进制文件开始具有这个新的权限,因此Apple逐渐将其迁移到这个新方法上。

0x05 新权限的优势

我们非常好奇,与之前的方法相比,这种方法有哪些优点。在这里我仅仅是推测,但确实发现了一些优势所在。使用新的方法,在加载应用程序时会强行执行库验证,这意味着攻击者无法对这类二进制文件进行dylib劫持或代理攻击。这种攻击方式是非常常见的,特别是针对第三方应用程序。

我们仍然可以将代码注入到这类应用程序中,但只能通过插件的方式,而不能通过其他方式。尽管插件的攻击方式并不比标准dylib方式难,但这种改进已经向更好的设计迈出了一步。遗憾的是,它只适用于Apple自己的文件,如果我们尝试在自己的二进制文件中使用它,则会出现错误。

mac_vnode_check_signature: /tmp/launch: code signature validation failed fatally: When validating /tmp/launch:
  Code has restricted entitlements, but the validation of its code signature failed.
Unsatisfied Entitlements:

0x06 总结

我们看到Apple在macOS 10.15.2中引入了新的权限com.apple.private.security.clear-library-validation,它允许进程使用csops系统调用清除自身的库验证标志。Apple正在将应用程序从旧的com.apple.security.cs.disable-library-validation权限缓慢迁移到新的权限,从而在设计层面上增加安全性。

0x00 概述

在macOS 10.15.2版本上,Apple引入了com.apple.private.security.clear-library-validation权限(entitlement),该权限正在逐渐取代以前在系统二进制文件上使用的com.apple.security.cs.disable-library-validation权限。尽管二者的影响大致相同,但它们的工作原理却存在差异。尽管使用com.apple.security.cs.disable-library-validation和com.apple.private.security.clear-library-validation会自动禁用库验证,但应用程序必须通过csops系统调用将其禁用。

0x01 简介

在Big Sur版本发布后,我注意到许多系统二进制文件都具有新的权限,其中的com.apple.private.security.clear-library-validation是我此前没有接触过的。这些应用程序之前使用的是com.apple.security.cs.disable-library-validation,看来它们似乎已经被一个新的权限替换。由于二者的名字比较相似,并且经过测试也证实了这些二进制文件仍然可以加载非Apple开发人员签名的第三方插件。这意味着,这些权限具有相同的影响。但是,二者的内部工作原理是不同的。

0x02 csops系统调用

在遇到下面列出的新csops操作代码后,我开始深入研究这一新的权限,可以在xnu-7195.50.7.100.1/bsd/sys/codesign.h中找到该代码。

#define CS_OPS_CLEAR_LV     15  /* clear the library validation flag */

csops是一个系统调用,可以用于对进程执行各类与代码签名相关的操作。我们可以查询进程的状态,在运行时设置各种标志,查询其代码签名blob等等。这是我以前没有发现过的新功能,因此我开始对其进行分析。

根据这个常量的说明,我们可以使用这个操作代码来清除进程的库验证标志。这意味着,如果我们可以在某个进程上运行它,则在调用成功的情况下,可以将第三方库加载到该进程中。

这个常量仅在xnu-7195.50.7.100.1/bsd/kern/kern_proc.c源文件中引用,该文件中包含csops_internal函数的源代码。这是在进行系统调用时将会运行的函数。下面是与CS_OPS_CLEAR_LV操作相关的部分源代码。

static int
csops_internal(pid_t pid, int ops, user_addr_t uaddr, user_size_t usersize, user_addr_t uaudittoken)
{
(...)
       if (pid == 0) {
              pid = proc_selfpid();
       }
       if (pid == proc_selfpid()) {
              forself = 1;
       }
 
 
       switch (ops) {
       case CS_OPS_STATUS:
       case CS_OPS_CDHASH:
       case CS_OPS_PIDOFFSET:
       case CS_OPS_ENTITLEMENTS_BLOB:
       case CS_OPS_IDENTITY:
       case CS_OPS_BLOB:
       case CS_OPS_TEAMID:
       case CS_OPS_CLEAR_LV:
              break;          /* not restricted to root */
       default:
              if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
                     return EPERM;
              }
              break;
       }
 
       pt = proc_find(pid);
       if (pt == PROC_NULL) {
              return ESRCH;
       }
(...)
#if CONFIG_MACF
       switch (ops) {
       case CS_OPS_MARKINVALID:
       case CS_OPS_MARKHARD:
       case CS_OPS_MARKKILL:
       case CS_OPS_MARKRESTRICT:
       case CS_OPS_SET_STATUS:
       case CS_OPS_CLEARINSTALLER:
       case CS_OPS_CLEARPLATFORM:
       case CS_OPS_CLEAR_LV:
              if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
              break;
       default:
              if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
       }
#endif
       switch (ops) {
(...)
       case CS_OPS_CLEAR_LV: {
              /*
               * This option is used to remove library validation from
               * a running process. This is used in plugin architectures
               * when a program needs to load untrusted libraries. This
               * allows the process to maintain library validation as
               * long as possible, then drop it only when required.
               * Once a process has loaded the untrusted library,
               * relying on library validation in the future will
               * not be effective. An alternative is to re-exec
               * your application without library validation, or
               * fork an untrusted child.
               */
#if !defined(XNU_TARGET_OS_OSX)
              // We only support dropping library validation on macOS
              error = ENOTSUP;
#else
              /*
               * if we have the flag set, and the caller wants
               * to remove it, and they're entitled to, then
               * we remove it from the csflags
               *
               * NOTE: We are fine to poke into the task because
               * we get a ref to pt when we do the proc_find
               * at the beginning of this function.
               *
               * We also only allow altering ourselves.
               */
              if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
                     proc_lock(pt);
                     pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
                     proc_unlock(pt);
                     error = 0;
              } else {
                     error = EPERM;
              }
(...)
}

我们将一步一步进行介绍。其中,有三个地方会对其进行检查。我们首先来看第一个swtich条件语句。

switch (ops) {
       case CS_OPS_STATUS:
       case CS_OPS_CDHASH:
       case CS_OPS_PIDOFFSET:
       case CS_OPS_ENTITLEMENTS_BLOB:
       case CS_OPS_IDENTITY:
       case CS_OPS_BLOB:
       case CS_OPS_TEAMID:
       case CS_OPS_CLEAR_LV:
              break;          /* not restricted to root */
       default:
              if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
                     return EPERM;
              }
              break;
       }

在这里,系统将允许非root执行switch条件中列出的操作,其中的一项是我们关注的重点。这表明,即使我们没有以root用户身份运行,也可以使用CS_OPS_CLEAR_LV操作调用csops。

接下来,我们来分析另一个switch条件。

#if CONFIG_MACF
       switch (ops) {
       case CS_OPS_MARKINVALID:
       case CS_OPS_MARKHARD:
       case CS_OPS_MARKKILL:
       case CS_OPS_MARKRESTRICT:
       case CS_OPS_SET_STATUS:
       case CS_OPS_CLEARINSTALLER:
       case CS_OPS_CLEARPLATFORM:
       case CS_OPS_CLEAR_LV:
              if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
              break;
       default:
              if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
       }
#endif

在这里,我们使用mac_proc_check_get_cs_info函数进行了MACF策略调用。如果成功,MACF策略调用将返回0,这就是对条件的检查。在xnu-7195.50.7.100.1/security/mac_process.c内部实现了mac_proc_check_get_cs_info函数。我们跟踪这个函数。

int
mac_proc_check_set_cs_info(proc_t curp, proc_t target, unsigned int op)
{
       kauth_cred_t cred;
       int error = 0;
 
#if SECURITY_MAC_CHECK_ENFORCE
       /* 21167099 - only check if we allow write */
       if (!mac_proc_enforce) {
              return 0;
       }
#endif
       if (!mac_proc_check_enforce(curp)) {
              return 0;
       }
 
       cred = kauth_cred_proc_ref(curp);
       MAC_CHECK(proc_check_set_cs_info, cred, target, op);
       kauth_cred_unref(&cred);
 
       return error;
}

该函数最终将使用MAC_CHECK宏进行MACF调用,我在针对CVE-2020-9771补丁的逆向分析过程中已经讨论过。它将遍历MACF策略挂钩,该挂钩对proc_check_set_cs_info进行检查。目前,它仅仅被沙箱挂钩,如下所示。

void _hook_proc_check_set_cs_info(int arg0, int arg1) {
    ___bzero(&var_1A0, 0x188);
    *(int32_t *)(&var_1A0 + 0xa8) = 0x4;
    *(&var_1A0 + 0xb8) = arg1;
    _cred_sb_evaluate(arg0, 0x65, &var_1A0, rcx, r8, r9);
    return;
}

在这里,将使用操作码0x65对_cred_sb_evaluate进行内部调用,我在上一篇文章中也对此进行了讨论。

回到我们的csops调用中,不论调用过程允许或不允许这个操作,都会运行MACF策略检查。

假设允许这个操作,那么我们继续,最后到达实际执行该操作的位置。

       case CS_OPS_CLEAR_LV: {
              /*
               * This option is used to remove library validation from
               * a running process. This is used in plugin architectures
               * when a program needs to load untrusted libraries. This
               * allows the process to maintain library validation as
               * long as possible, then drop it only when required.
               * Once a process has loaded the untrusted library,
               * relying on library validation in the future will
               * not be effective. An alternative is to re-exec
               * your application without library validation, or
               * fork an untrusted child.
               */
#if !defined(XNU_TARGET_OS_OSX)
              // We only support dropping library validation on macOS
              error = ENOTSUP;
#else
              /*
               * if we have the flag set, and the caller wants
               * to remove it, and they're entitled to, then
               * we remove it from the csflags
               *
               * NOTE: We are fine to poke into the task because
               * we get a ref to pt when we do the proc_find
               * at the beginning of this function.
               *
               * We also only allow altering ourselves.
               */
              if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
                     proc_lock(pt);
                     pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
                     proc_unlock(pt);
                     error = 0;
              } else {
                     error = EPERM;
              }

在这里,Apple添加了非常详细的注释,对所有内容都进行了说明。如果满足要求,它将清除目标进程的库验证标志(pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));)。

条件非常严格。该操作只能由进程自身(forself == 1)以及具有由常量CLEAR_LV_ENTITLEMENT定义的权限的进程调用。这是在xnu-7195.50.7.100.1/bsd/sys/codesign.h中进行的定义。

#define CLEAR_LV_ENTITLEMENT "com.apple.private.security.clear-library-validation"

在循环结束后,我们就得到了以前曾见到过的权限。

综上所述,我们可以确定,拥有com.apple.private.security.clear-library-validation权限的进程可以使用CLEAR_LV_ENTITLEMENT调用csops系统调用,以清除自身的库验证代码签名标志。即使该进程未以root用户身份运行,这个方法也是有效的。

0x03 SSH

为了确认这个发现,我将具有这个权限的SSH加载到Hopper中,并验证它是否使用csops禁用了库验证。

int sub_10016d41f(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
(...)
loc_10016d67b:
    rax = getpid();
    rax = csops(rax, 0xf, 0x0, 0x0);
    if (rax == 0x0) goto loc_10016d6d6;
 
loc_10016d694:
    rdx = 0x0;
    rcx = 0x0;
    rbx = 0x0;
    sub_10014e556("csops(CS_OPS_CLEAR_LV) failed: %d", rax, rdx, rcx, r8, r9, stack[-136]);
(...)

的确,我们可以找到使用操作码0xf调用csops的函数,也就是CS_OPS_CLEAR_LV操作。如果该调用失败,我们还能得到一个详细的错误消息。

0x04 历史

尽管我仅仅在Big Sur版本中发现了这个变化,但是这个功能是较早之前引入的。在xnu-6153.51.1中引入了用于清除库验证标志的详细csops操作,该操作已经在macOS 10.15.2中使用。但是使用该权限的二进制文件仅在macOS 10.15.4版本以后出现,只涉及四个应用程序——su、screen、login和passwd。

从Big Sur(macOS 11.0)开始,有20个二进制文件开始具有这个新的权限,因此Apple逐渐将其迁移到这个新方法上。

0x05 新权限的优势

我们非常好奇,与之前的方法相比,这种方法有哪些优点。在这里我仅仅是推测,但确实发现了一些优势所在。使用新的方法,在加载应用程序时会强行执行库验证,这意味着攻击者无法对这类二进制文件进行dylib劫持或代理攻击。这种攻击方式是非常常见的,特别是针对第三方应用程序。

我们仍然可以将代码注入到这类应用程序中,但只能通过插件的方式,而不能通过其他方式。尽管插件的攻击方式并不比标准dylib方式难,但这种改进已经向更好的设计迈出了一步。遗憾的是,它只适用于Apple自己的文件,如果我们尝试在自己的二进制文件中使用它,则会出现错误。

mac_vnode_check_signature: /tmp/launch: code signature validation failed fatally: When validating /tmp/launch:
  Code has restricted entitlements, but the validation of its code signature failed.
Unsatisfied Entitlements:

0x06 总结

我们看到Apple在macOS 10.15.2中引入了新的权限com.apple.private.security.clear-library-validation,它允许进程使用csops系统调用清除自身的库验证标志。Apple正在将应用程序从旧的com.apple.security.cs.disable-library-validation权限缓慢迁移到新的权限,从而在设计层面上增加安全性。

0x00 概述

在macOS 10.15.2版本上,Apple引入了com.apple.private.security.clear-library-validation权限(entitlement),该权限正在逐渐取代以前在系统二进制文件上使用的com.apple.security.cs.disable-library-validation权限。尽管二者的影响大致相同,但它们的工作原理却存在差异。尽管使用com.apple.security.cs.disable-library-validation和com.apple.private.security.clear-library-validation会自动禁用库验证,但应用程序必须通过csops系统调用将其禁用。

0x01 简介

在Big Sur版本发布后,我注意到许多系统二进制文件都具有新的权限,其中的com.apple.private.security.clear-library-validation是我此前没有接触过的。这些应用程序之前使用的是com.apple.security.cs.disable-library-validation,看来它们似乎已经被一个新的权限替换。由于二者的名字比较相似,并且经过测试也证实了这些二进制文件仍然可以加载非Apple开发人员签名的第三方插件。这意味着,这些权限具有相同的影响。但是,二者的内部工作原理是不同的。

0x02 csops系统调用

在遇到下面列出的新csops操作代码后,我开始深入研究这一新的权限,可以在xnu-7195.50.7.100.1/bsd/sys/codesign.h中找到该代码。

#define CS_OPS_CLEAR_LV     15  /* clear the library validation flag */

csops是一个系统调用,可以用于对进程执行各类与代码签名相关的操作。我们可以查询进程的状态,在运行时设置各种标志,查询其代码签名blob等等。这是我以前没有发现过的新功能,因此我开始对其进行分析。

根据这个常量的说明,我们可以使用这个操作代码来清除进程的库验证标志。这意味着,如果我们可以在某个进程上运行它,则在调用成功的情况下,可以将第三方库加载到该进程中。

这个常量仅在xnu-7195.50.7.100.1/bsd/kern/kern_proc.c源文件中引用,该文件中包含csops_internal函数的源代码。这是在进行系统调用时将会运行的函数。下面是与CS_OPS_CLEAR_LV操作相关的部分源代码。

static int
csops_internal(pid_t pid, int ops, user_addr_t uaddr, user_size_t usersize, user_addr_t uaudittoken)
{
(...)
       if (pid == 0) {
              pid = proc_selfpid();
       }
       if (pid == proc_selfpid()) {
              forself = 1;
       }
 
 
       switch (ops) {
       case CS_OPS_STATUS:
       case CS_OPS_CDHASH:
       case CS_OPS_PIDOFFSET:
       case CS_OPS_ENTITLEMENTS_BLOB:
       case CS_OPS_IDENTITY:
       case CS_OPS_BLOB:
       case CS_OPS_TEAMID:
       case CS_OPS_CLEAR_LV:
              break;          /* not restricted to root */
       default:
              if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
                     return EPERM;
              }
              break;
       }
 
       pt = proc_find(pid);
       if (pt == PROC_NULL) {
              return ESRCH;
       }
(...)
#if CONFIG_MACF
       switch (ops) {
       case CS_OPS_MARKINVALID:
       case CS_OPS_MARKHARD:
       case CS_OPS_MARKKILL:
       case CS_OPS_MARKRESTRICT:
       case CS_OPS_SET_STATUS:
       case CS_OPS_CLEARINSTALLER:
       case CS_OPS_CLEARPLATFORM:
       case CS_OPS_CLEAR_LV:
              if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
              break;
       default:
              if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
       }
#endif
       switch (ops) {
(...)
       case CS_OPS_CLEAR_LV: {
              /*
               * This option is used to remove library validation from
               * a running process. This is used in plugin architectures
               * when a program needs to load untrusted libraries. This
               * allows the process to maintain library validation as
               * long as possible, then drop it only when required.
               * Once a process has loaded the untrusted library,
               * relying on library validation in the future will
               * not be effective. An alternative is to re-exec
               * your application without library validation, or
               * fork an untrusted child.
               */
#if !defined(XNU_TARGET_OS_OSX)
              // We only support dropping library validation on macOS
              error = ENOTSUP;
#else
              /*
               * if we have the flag set, and the caller wants
               * to remove it, and they're entitled to, then
               * we remove it from the csflags
               *
               * NOTE: We are fine to poke into the task because
               * we get a ref to pt when we do the proc_find
               * at the beginning of this function.
               *
               * We also only allow altering ourselves.
               */
              if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
                     proc_lock(pt);
                     pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
                     proc_unlock(pt);
                     error = 0;
              } else {
                     error = EPERM;
              }
(...)
}

我们将一步一步进行介绍。其中,有三个地方会对其进行检查。我们首先来看第一个swtich条件语句。

switch (ops) {
       case CS_OPS_STATUS:
       case CS_OPS_CDHASH:
       case CS_OPS_PIDOFFSET:
       case CS_OPS_ENTITLEMENTS_BLOB:
       case CS_OPS_IDENTITY:
       case CS_OPS_BLOB:
       case CS_OPS_TEAMID:
       case CS_OPS_CLEAR_LV:
              break;          /* not restricted to root */
       default:
              if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
                     return EPERM;
              }
              break;
       }

在这里,系统将允许非root执行switch条件中列出的操作,其中的一项是我们关注的重点。这表明,即使我们没有以root用户身份运行,也可以使用CS_OPS_CLEAR_LV操作调用csops。

接下来,我们来分析另一个switch条件。

#if CONFIG_MACF
       switch (ops) {
       case CS_OPS_MARKINVALID:
       case CS_OPS_MARKHARD:
       case CS_OPS_MARKKILL:
       case CS_OPS_MARKRESTRICT:
       case CS_OPS_SET_STATUS:
       case CS_OPS_CLEARINSTALLER:
       case CS_OPS_CLEARPLATFORM:
       case CS_OPS_CLEAR_LV:
              if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
              break;
       default:
              if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
                     goto out;
              }
       }
#endif

在这里,我们使用mac_proc_check_get_cs_info函数进行了MACF策略调用。如果成功,MACF策略调用将返回0,这就是对条件的检查。在xnu-7195.50.7.100.1/security/mac_process.c内部实现了mac_proc_check_get_cs_info函数。我们跟踪这个函数。

int
mac_proc_check_set_cs_info(proc_t curp, proc_t target, unsigned int op)
{
       kauth_cred_t cred;
       int error = 0;
 
#if SECURITY_MAC_CHECK_ENFORCE
       /* 21167099 - only check if we allow write */
       if (!mac_proc_enforce) {
              return 0;
       }
#endif
       if (!mac_proc_check_enforce(curp)) {
              return 0;
       }
 
       cred = kauth_cred_proc_ref(curp);
       MAC_CHECK(proc_check_set_cs_info, cred, target, op);
       kauth_cred_unref(&cred);
 
       return error;
}

该函数最终将使用MAC_CHECK宏进行MACF调用,我在针对CVE-2020-9771补丁的逆向分析过程中已经讨论过。它将遍历MACF策略挂钩,该挂钩对proc_check_set_cs_info进行检查。目前,它仅仅被沙箱挂钩,如下所示。

void _hook_proc_check_set_cs_info(int arg0, int arg1) {
    ___bzero(&var_1A0, 0x188);
    *(int32_t *)(&var_1A0 + 0xa8) = 0x4;
    *(&var_1A0 + 0xb8) = arg1;
    _cred_sb_evaluate(arg0, 0x65, &var_1A0, rcx, r8, r9);
    return;
}

在这里,将使用操作码0x65对_cred_sb_evaluate进行内部调用,我在上一篇文章中也对此进行了讨论。

回到我们的csops调用中,不论调用过程允许或不允许这个操作,都会运行MACF策略检查。

假设允许这个操作,那么我们继续,最后到达实际执行该操作的位置。

       case CS_OPS_CLEAR_LV: {
              /*
               * This option is used to remove library validation from
               * a running process. This is used in plugin architectures
               * when a program needs to load untrusted libraries. This
               * allows the process to maintain library validation as
               * long as possible, then drop it only when required.
               * Once a process has loaded the untrusted library,
               * relying on library validation in the future will
               * not be effective. An alternative is to re-exec
               * your application without library validation, or
               * fork an untrusted child.
               */
#if !defined(XNU_TARGET_OS_OSX)
              // We only support dropping library validation on macOS
              error = ENOTSUP;
#else
              /*
               * if we have the flag set, and the caller wants
               * to remove it, and they're entitled to, then
               * we remove it from the csflags
               *
               * NOTE: We are fine to poke into the task because
               * we get a ref to pt when we do the proc_find
               * at the beginning of this function.
               *
               * We also only allow altering ourselves.
               */
              if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
                     proc_lock(pt);
                     pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
                     proc_unlock(pt);
                     error = 0;
              } else {
                     error = EPERM;
              }

在这里,Apple添加了非常详细的注释,对所有内容都进行了说明。如果满足要求,它将清除目标进程的库验证标志(pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));)。

条件非常严格。该操作只能由进程自身(forself == 1)以及具有由常量CLEAR_LV_ENTITLEMENT定义的权限的进程调用。这是在xnu-7195.50.7.100.1/bsd/sys/codesign.h中进行的定义。

#define CLEAR_LV_ENTITLEMENT "com.apple.private.security.clear-library-validation"

在循环结束后,我们就得到了以前曾见到过的权限。

综上所述,我们可以确定,拥有com.apple.private.security.clear-library-validation权限的进程可以使用CLEAR_LV_ENTITLEMENT调用csops系统调用,以清除自身的库验证代码签名标志。即使该进程未以root用户身份运行,这个方法也是有效的。

0x03 SSH

为了确认这个发现,我将具有这个权限的SSH加载到Hopper中,并验证它是否使用csops禁用了库验证。

int sub_10016d41f(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
(...)
loc_10016d67b:
    rax = getpid();
    rax = csops(rax, 0xf, 0x0, 0x0);
    if (rax == 0x0) goto loc_10016d6d6;
 
loc_10016d694:
    rdx = 0x0;
    rcx = 0x0;
    rbx = 0x0;
    sub_10014e556("csops(CS_OPS_CLEAR_LV) failed: %d", rax, rdx, rcx, r8, r9, stack[-136]);
(...)

的确,我们可以找到使用操作码0xf调用csops的函数,也就是CS_OPS_CLEAR_LV操作。如果该调用失败,我们还能得到一个详细的错误消息。

0x04 历史

尽管我仅仅在Big Sur版本中发现了这个变化,但是这个功能是较早之前引入的。在xnu-6153.51.1中引入了用于清除库验证标志的详细csops操作,该操作已经在macOS 10.15.2中使用。但是使用该权限的二进制文件仅在macOS 10.15.4版本以后出现,只涉及四个应用程序——su、screen、login和passwd。

从Big Sur(macOS 11.0)开始,有20个二进制文件开始具有这个新的权限,因此Apple逐渐将其迁移到这个新方法上。

0x05 新权限的优势

我们非常好奇,与之前的方法相比,这种方法有哪些优点。在这里我仅仅是推测,但确实发现了一些优势所在。使用新的方法,在加载应用程序时会强行执行库验证,这意味着攻击者无法对这类二进制文件进行dylib劫持或代理攻击。这种攻击方式是非常常见的,特别是针对第三方应用程序。

我们仍然可以将代码注入到这类应用程序中,但只能通过插件的方式,而不能通过其他方式。尽管插件的攻击方式并不比标准dylib方式难,但这种改进已经向更好的设计迈出了一步。遗憾的是,它只适用于Apple自己的文件,如果我们尝试在自己的二进制文件中使用它,则会出现错误。

mac_vnode_check_signature: /tmp/launch: code signature validation failed fatally: When validating /tmp/launch:
  Code has restricted entitlements, but the validation of its code signature failed.
Unsatisfied Entitlements:

0x06 总结

我们看到Apple在macOS 10.15.2中引入了新的权限com.apple.private.security.clear-library-validation,它允许进程使用csops系统调用清除自身的库验证标志。Apple正在将应用程序从旧的com.apple.security.cs.disable-library-validation权限缓慢迁移到新的权限,从而在设计层面上增加安全性。