
此处所述问题的修复已出现在 CVE-2024-34740 / A-307288067 下:
在 Android system_server 内部,许多服务在跨重启时将其状态存储为 XML 文件```
$ adb shell su 0 find /data/system -name '*.xml' | sort
/data/system/appops_accesses.xml
/data/system/cachequota.xml
/data/system/device_policies.xml
/data/system/device_policy_state.xml
/data/system/display-manager-state.xml
/data/system/input-manager-state.xml
/data/system/inputmethod/subtypes.xml
/data/system/install_sessions.xml
/data/system/job/jobs_1000.xml
/data/system/job/jobs_10131.xml
/data/system/log-files.xml
/data/system/netpolicy.xml
/data/system/notification_policy.xml
/data/system/overlays.xml
/data/system/packages.xml
/data/system/package-watchdog.xml
/data/system/sensor_privacy_impl.xml
/data/system/sensor_privacy.xml
/data/system/shortcut_service.xml
/data/system/users/0/app_idle_stats.xml
/data/system/users/0/appwidgets.xml
/data/system/users/0/package-restrictions.xml
/data/system/users/0/settings_global.xml
/data/system/users/0/settings_secure.xml
/data/system/users/0/settings_system.xml
/data/system/users/0/wallpaper_info.xml
/data/system/users/0.xml
/data/system/users/userlist.xml
/data/system/watchlist_settings.xml
历史上,这些一直是带缩进的纯文本 XML 文件,便于开发者阅读;然而,[在 Android 12 中引入了该格式的新二进制版本,理由是 `system_server` 总耗时中有 1.5% 被花在了这些 XML 操作上](https://android.googlesource.com/platform/frameworks/base/+/4ccea8796991d678ead4399130ec31edf63ff4fa%5E%21/)
需要注意的是,这种格式仅供系统内部使用,其文件具有魔数 `"ABX\x00"`。它不同于 [APK 内部使用的格式](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/ResourceTypes.cpp;l=1770;drc=d4e49e63519397789d284a03aea5fafc119cb1b0),后者用于 `AndroidManifest.xml`、`res/xml/*.xml`、`res/layout/*.xml` 等,没有显式的“魔数”,但通常以 `0300 0800` 开头(即 [头部](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h;l=608;drc=d4e49e63519397789d284a03aea5fafc119cb1b0) 的 `type=RES_XML_TYPE` 和 `headerSize=8`)
每当系统读取这些内部状态 XML 文件之一时,它会[根据文件中的 `"ABX\0"` 魔数选择使用 Binary XML 文件解析器还是常规 XML 解析器](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;l=188-192;drc=97a370a95275e79c69e79d7ead11aa38934a5575)。这些文件是否以 Binary XML 形式保存由[系统属性](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;drc=97a370a95275e79c69e79d7ead11aa38934a5575;l=74?q=Xml.java)控制,且默认启用
当使用 Binary XML 文件时,你可以通过例如 `adb shell su 0 abx2xml /data/system/packages.xml -` 来读取其内容
这种二进制格式的功能之一是提供类型化访问器,因此序列化器提供了 `attributeInt(String namespace, String name, int value)` 方法,该方法将值以二进制整数形式写入,避免了通过 String 的往返转换——那会产生新的内存分配以及随后需要垃圾回收的对象
另一种可以直接序列化的类型是字节数组```java
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
还有一个类似的方法 attributeBytesHex,区别仅在于写入的 TYPE_* 标签。abx2xml 工具使用该标签将字节数组转换为相应的字符串表示。
mOut 是 FastDataOutput 的一个实例,它提供了 Java DataOutputStream 的函数。writeByte/writeShort/writeInt/writeUTF/write 使用与标准 DataOutputStream 相同的格式。
与 Parcel 类似,如果在写入/读取过程中出现不匹配,后续读取的数据会从错误的偏移量处获取;但不同于 Parcel,使用 BinaryXmlSerializer/BinaryXmlPullParser 时出现的错误并不会让攻击者能够任意篡改读取的数据(在这种情况下,攻击者无法引入新的标签/属性名/属性值)。
然而,BinaryXmlSerializer 类本身或 FastDataOutput 中的错误却可以做到这一点。
在上述方法中,如果我们尝试写入长度为 65536 的字节数组,我们会使用 writeShort() 写入长度,这实际上会写入 0,随后数组的实际内容会被写入。
为了利用这种不匹配,我们需要选择某个文件,使我们能够将任意字节数组注入到 attributeBytesBase64 或 attributeBytesHex,并且修改该文件对攻击者来说是有价值的。
PackageInstaller 类提供了为安装准备包的能力。任何应用无需任何权限即可将待安装的新 APK 写入临时目录。一旦写入安装所需的全部内容,安装应用就可以 commit() PackageInstaller.Session,这意味着它无法再对安装文件进行任何更改,并且 Session 已准备好等待用户批准或实际安装。
这些操作的状态存储在 /data/system/install_sessions.xml 中。例如,安装应用可以先将大型 APK 的一半下载到 Package Manager Service 为其 PackageInstaller.Session 创建的临时目录中,然后在重启后继续下载,写入剩余一半并提交安装。
其中一种可能是向 install_sessions.xml 写入数据,将会话标记为暂存,这意味着它将在下次启动后安装。
另一种(此处介绍的)是更改准备安装文件所用的临时目录路径,因为 openWrite()/openRead() 接受任何有效的文件名,只要没有路径穿越,并且将该文件放置在 stageDir 字段指向的目录中,而该字段是从 XML 中读取的。
现在我们需要真正将受我们控制的字节数组送入 attributeBytesBase64()。
PackageInstaller.Session 提供了 setChecksums() 方法。
在 system_server 一侧,提供的各个 Checksum 会根据调用者提供的签名进行可选验证,然后放入 mChecksums。
当写入 install_sessions.xml 时,checksum.getValue() 会被传递给 writeByteArrayAttribute,后者再将其传递给 attributeBytesBase64()。
有少数事件会触发 install_sessions.xml 的写入,其中之一是创建新的 Session。因此,此漏洞利用会在一个会话上设置 Checksum 后创建新的 Session,以确保第一个 Session 已保存到文件中。
现在,我们写入长度为 65536 的字节数组;在它被读取后,其大小会被解释为零,并且该数组的内容会成为 BinaryXmlPullParser 解析的原始数据。