type
status
date
slug
summary
tags
category
icon
password
0x00 漏洞说明
首先回顾CVE-2024-0044漏洞,在Android 12和13中,存在一个安全漏洞,即通过
pm install
命令的-i
标志设置的安装程序包名没有经过适当的校验和清理。这意味着攻击者可以利用特殊字符(如换行符和空格)来注入恶意数据,从而影响系统文件/data/system/packages.list
的内容。之后可以通过伪造的条目欺骗run-as
从而获得受害应用的权限,可以操作内部目录等。然而,这种方法需要攻击者能够访问
ADB shell
,这通常对攻击者来说是一个很高的门槛。为了降低这个门槛,尝试利用 Zygote
进程,因为Zygote是另一个可以更改UID
的进程。通过利用 Zygote
进程的这一特性,攻击者可以尝试模拟具有更高权限的应用程序,从而更容易地实现他们的目标。于是就引出了该漏洞(编号
CVE-2024-31317
),该漏洞允许拥有WRITE_SECURE_SETTINGS
权限的攻击者(该权限由ADB shell和某些特权应用持有)以任意应用的身份执行任意代码。通过这种方式,攻击者可以读取和写入任何应用的数据,更改大多数系统配置,取消注册或绕过移动设备管理等。这个漏洞的利用不涉及内存的破坏,这意味着它可以在几乎任何运行Android 9或更高版本的设备上不加修改地工作,并且在重启后仍然有效。Zygote
是 Android 系统中的一个重要进程,它在系统启动时创建,并负责预加载许多常用的类和资源,以便应用程序可以快速启动。与其他进程不同,Zygote 进程允许更改其用户 ID(UID),这使得它成为潜在的攻击目标。1. Zygote
Zygote
负责创建应用的主进程并设置该进程的身份。尽管只有system_server
可以向Zygote发送命令,但这些命令的源头可以是普通应用,例如启动Activty
或是Service
等,这些普通的请求可以传递到system_server
再传递到Zygote
。当system_server
收到一个尚未运行的应用的请求时,它通过告诉Zygote生成一个具有适当包名、数据目录、UID、SELinux域等的进程来启动该应用。值得注意的是,系统服务器控制着像新应用UID这样的安全关键参数。Zygote可能因为其在启动序列中处在较早期的时期,并不会从Android包数据库中查询这些参数。这意味着如果我们能控制系统服务器发送的命令,就可以冒充任意应用。
Zygote作为守护进程运行,并在
/dev/socket/zygote
的UNIX流套接字上接受命令。流套接字不是面向消息的,因此Zygote的线协议必须定义一个命令结束和下一个命令开始的位置。它的实现非常简单:每个命令是UTF-8文本,由一个十进制数字和相应数量的参数组成,每个参数占一行。最后一个参数后的行开始下一个命令。一个命令仅由一系列参数组成。与大多数命令协议不同,Zygote的协议没有“命令类型”的概念。默认情况下,每个命令都会生成一个新进程,参数指定该进程的详细信息。某些特殊参数会覆盖默认行为,使Zygote执行其他操作。
以下是一个典型的进程生成命令的示例(省略了许多参数以简洁)(括号中的文本是解释性的,不是协议的一部分)。然后是一个特殊的
set API denylist exemptions
命令。2. 漏洞信息
在Android中发现的一个全局设置
hidden_api_blacklist_exemptions
,其值会直接包含在Zygote命令中。system_server
并不期望该设置包含换行符,因此既不会对其进行转义,也不会在命令的参数计数中标注它们。通过向该设置写入恶意值,攻击者可以在最后一个声明的参数之后插入自己选择的行。当Zygote看到这些行时,会将其视为一个单独的命令并执行。漏洞的代码路径始于系统服务器中的一个
ContentObserver
回调,当hidden_api_blacklist_exemptions
因任何原因发生变化时,该回调会被触发。从下面的代码中可以看出,将该设置的值通过逗号分隔后传入setApiDenylistExemptions
,但是并没有对其他符号进行处理。在
setApiDenylistExemptions
内部调用了maybeSetApiDenylistExemptions
,其中向 state.mZygoteOutputWriter
写入数据,该变量类型为BufferedWriter
,其缓存区大小为8192
字节。0x01 漏洞利用
1. 攻击场景
原文提供三种攻击场景
场景 1:权限提升
任何具有
android.permission.WRITE_SECURE_SETTINGS
权限的应用程序都可以写入 hidden_api_blacklist_exemptions
并触发漏洞。虽然 Android 将该权限的保护级别声明为 signature|privileged|development|role|installer
,这意味着非特权应用无法请求该权限,但某些预装应用持有该权限。如果攻击者能够入侵这些预装应用之一,就可以利用这个漏洞进一步提升权限。场景 2:ADB Shell
ADB shell 也可以读取和写入设置;它甚至有一个
settings
命令来简化这一操作。拥有物理访问权限的攻击者可以通过解锁设备来触发漏洞,或者用户可以在自己拥有的设备上绕过系统策略(例如 MDM 限制)来触发漏洞。场景 3:签名配置
还有一种设置
hidden_api_blacklist_exemptions
的方法,这也是它存在的原因:任何应用(即使是即时应用)都可以在其清单中包含一对特殊的 <meta-data>
标签,其中包含:- 要存储在
hidden_api_blacklist_exemptions
中的 Base64 编码值;
- 由硬编码的 Google 控制密钥对该值进行的 ECDSA 签名。
如果安装了这样的应用并且签名有效,Android 会立即应用该设置值,可能会触发漏洞。我们认为签名验证和周围的逻辑是正确实现的,因此可能只有 Google 自己能够通过这种方式利用设备。然而,大多数 Android 设备不是 Google 的一方设备,这个漏洞可能会使 Google 对这些设备的访问权限比 OEM 和用户预期的更大。值得注意的是,CTS 要求接受 Google 签名的元数据,这意味着即使 OEM 想要移除这种利用路径也无法做到。
2. Android11-
在Android 11及以下版本中,利用漏洞的过程相对简单。假设在
shell
用户下进行利用,只需要通过settings
命令设置属性即可,其中payload.txt
是上传到/data/local/tmp
中的文本。其中
payload.txt
的内容解释为:其开始有5个换行,之后的8代表后续有8个参数,关键是--invoke-with
后面跟着需要执行的命令,并且注意到在结束有#
符号,用于注释掉后续的命令,后面分析这部分内容的解析时会提到。(为了便于测试,后面的命令执行内容可以替换为/system/bin/logwrapper echo zYg0te $(id)
然后通过logcat
即可知道是否利用成功)当这部分内容通过
settings
设置后会被setApiDenylistExemptions
解析为五部分并且进一步在头部添加两个字段 ,分别是参数数量
6
和--set-api-denylist-exemptions
传入zygote
的socket
中,最终传入到zygote socket中的数据其实是,而其中的换行符会被zygote
直接忽略,所以造成了命令注入。设置后需要打开任意App以触发zygote
的读取解析操作,从而执行注入的命令。在注入之后打开任何App都会失败,只需要重新设置hidden_api_blacklist_exemptions
为null
即可以及为什么最后一个entry
不是空的:Note that the last delay entry isn’t empty like the rest. That’s to work around the fact that Java’sString.split()
function, used by System Server to parse the setting value, discards trailing empty strings.
然后分析
--invoke-with
的解析部分可以发现其实就是拼接了一些命令来创建新的进程,如果使用井号注释即可避免后面命令的执行,以避免异常情况的产生。同理也可以伪造蓝牙用户,读取其中的数据文件
反弹shell
在反弹shell这里遇到了一些问题,在常规情况下可以使用管道的方式去实现安卓环境下的反弹shell,但是很不幸,由于
SELinux
的限制无法创建管道pipe
文件。除此之外还尝试直接写入可执行文件执行,但是也被SELinux拒绝了,因为system写入文件时写入的对象是system_file
类型,不具备执行权限。直到京东flanker
发了公众号文章后发现还是大佬玩的骚,直接装一个新的App,在新App的lib目录中放置一个可执行的二进制文件,在安装时会释放到/data/app/xxx/xxx/lib/arm64
目录下,此时这个可执行文件的对象类型是apk_data_file
可以被执行,system
用户可直接访问这个目录下的内容并且执行,从而实现反弹shell
还有很多骚操作可以看flanker大佬的公众号文章,这里不再赘述。
3. Android12+
下面的内容翻译自原文
然而,在Android 12中,Google增强了Zygote的Java命令解析器,增加了一个快速路径的C++解析器,并通过一个新的类
NativeCommandBuffer
从套接字读取数据,使得这个漏洞更难利用,不是因为其架构,而是因为一个bug。readLine
()
方法从套接字读取字节,将字节填充到本地缓冲区,然后从缓冲区中提取行,并在必要时重新填充缓冲区。但在解析完所有命令行后,Zygote会丢弃缓冲区中剩余的字节,并从套接字读取下一个命令。这种行为导致了三个问题:- 如果客户端连续写入两个命令,而Zygote还没来得及读取,Zygote会忽略第二个命令。
- 如果客户端写入一个半命令(例如,因为第二个命令需要多次write调用),Zygote会忽略第二个命令的开始部分,并将其结尾部分解析为新的命令,这本身就是一个安全漏洞。然而,系统服务器(Zygote的唯一客户端)从不一次写入多个命令,因此这种情况(以及前一种情况)在实践中不会发生。
尽管有这个障碍,我们仍然可以在Android 12+上利用这个漏洞!我们需要的是一种方法来避免我们的恶意命令被Zygote的第一次
read
调用读取。我们最初尝试通过延长payload来超过Zygote传递给read的缓冲区长度限制,但不幸的是,完全填满其缓冲区(在Android 13中扩展到32768
字节),Zygote会中止。因此,我们只能寻找时机:我们可以假设Zygote大部分时间都被阻塞在read,这意味着我们进行的任何写入都可能触发一个立即的短读取,即使我们在第一次
read
不久后就进行另一次写入。- BufferedWriter:
maybeSetApiDenylistExemptions()
方法多次调用state.mZygoteOutputWriter.write()
。mZygoteOutputWriter
是一个BufferedWriter
,它在写入到底层传输之前会在内部缓冲区中聚合写入操作。
- 缓冲区大小:
BufferedWriter
的缓冲区大小为 8192 字节,比 Zygote 的缓冲区小。这提供了一种方法,可以在两次套接字写入之间产生适当的延迟。
所以原文给出的解决方案是:
- 通过将 System Server 的命令填充到正好 8192 字节,然后插入恶意命令,强制
BufferedWriter
先写入这 8192 字节。Zygote 会忽略填充部分,但不会忽略新到达的恶意命令,因为它会在一个单独的read()
调用中到达。
- 增加延迟:在设置值的末尾插入大量逗号,使
maybeSetApiDenylistExemptions()
在第一次写入后但在第二次写入前花费时间循环。这些逗号还增加了合法命令的参数数量,但只要确保前 8192 字节包含足够多的换行符号。
Payload
如下,需要将执行结果设置为hidden_api_blacklist_exemptions
经过反复实验发现确实如文中所提到的,必须首先填满8192字节的
mZygoteOutputWriter
,然后等待flush
再次写入,这个过程中需要填充大量逗号(需要添加等量的换行符)来使其在maybeSetApiDenylistExemptions
中间的循环过程中耗费时间,从而使得Zygote
读取端在这个过程中完成了之前写入的8192字节内容的读取,此时read
阻塞,等待下一次的输入。下一次的读取从构造好的恶意参数开始,即第一个字节就是恶意命令参数的数目
这里还有一个
payload
维持的问题,直接粘贴原文,意思就是使注入的命令参数数目超过在scoket
中末尾写入的换行数量,强制Zygote
解析额外的东西,以一直保持恶意命令的注入并且不影响设备正常运行,但是在原文提出的概念POC中并没有关于这部分的说明。但是我试了很多改法(比如下面的这种改法),都没成功维持,但是由于不会导致设备crash
并且能够成功利用,所以暂且搁置。A successful exploit degrades or prevents subsequent process launches until a reboot. That’s because the injected Zygote command outputs extra result bytes that System Server doesn’t consume. System Server uses a single connection to Zygote for all non-USAP commands, so those bytes stick around until it tries to spawn another process, at which point it reads them instead of that process’s PID. System Server won’t bind a process without record of its PID, and processes that fail to bind get killed.We avoided this issue on Android 12+ by slightly modifying our exploit: we declared an argument count for our injected command that exceeded the number of newlines in our final socket write, which forced Zygote to perform an additional socket read while parsing it. That read ate whatever command happened to follow ours (overwhelmingly likely to be a process spawn) and prevented Zygote from executing it. Our malicious command in effect replaced that legitimate command, and System Server consumed its PID (actually our PID) as normal, allowing subsequent PIDs to remain in sync.This modification also made persistence feasible, as the setting can retain its malicious value across reboots without disrupting the boot process.
0x02 漏洞修复
直接改了
HiddenApiSettings.update()
添加了对非法字符的过滤,除此之外还添加了在处理进程启动时的非法字符null
过滤。Refuse to deal with newlines and null characters inHiddenApiSettings.update()
. Also disallow nulls in process start arguments.
但是同样的,漏洞发现者认为着治标不治本,给出了建议
Today’s patch does not address the architectural weaknesses we identified, like Zygote’s use of a hand-rolled stream protocol or ZygoteProcess’s lack of a reusable function to safely serialize commands, as those entail bigger changes and are not directly exploitable. Google has has communicated that they’re considering such changes going forwards, though.
参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/CVE-2024-31317-Zygote
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章