一条完整的利用链,可从本地不受信任的应用将权限提升至 root/内核。它不涉及内存破坏或竞态条件,因此攻击者无需进行复杂的堆喷或绕过针对内存破坏漏洞的缓解措施(如 KASLR、MTE 或 CFI),这使得该利用链在易受攻击的设备上拥有 100% 的成功率。
已在运行 Android 17 初始官方版本的 Pixel 10 上测试。请注意,它不适用于 Pixel 6a,并且由于这些内核中的另一个 bug,此问题也可能出现在其他运行 6.1.xxx-android14 内核树的设备上。
使用方法:安装 KernelSU 应用,打开此应用,点击“运行用户态利用”,然后点击“运行内核利用并加载 KernelSU”。成功利用后,KernelSU 将被激活,您可以使用它向其他应用授予 root 访问权限。已知问题:如果您已经运行过内核利用并想再次运行,则需要重启设备。
屏幕录制:点击此处
该利用链由两个不同的漏洞组成:一个是 Telecom 服务中的 0-day 漏洞,另一个是 3 个月前披露的内核 1-day 漏洞。但在撰写本文时,AOSP 和 Pixel 设备(运行测试版 QPR 版本的设备除外)仍然易受攻击。
利用链的第一个漏洞是 Android 17 中引入的一个简单逻辑 bug。它源于一个疯狂的改动,该改动向 InCallController.java 添加了以下代码:
PackageManager packageManager = mContext.getPackageManager();
Context userContext = mContext.createContextAsUser(userHandle,
0 /* flags */);
PackageManager userPackageManager = userContext != null ?
userContext.getPackageManager() : packageManager;
List<ResolveInfo> entries;
entries = userPackageManager.queryIntentServices(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS);
for (ResolveInfo entry : entries) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
boolean isMetaFlag = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
"android.telecom.CLASS_EXISTENCE_CHECK", false);
if (isMetaFlag && !serviceClassExists(serviceInfo, userHandle)) {
continue;
}
}
}
相关的 serviceClassExists() 方法定义如下:
/**
* Verifies that the class for a given ServiceInfo exists within its package.
* This prevents a system crash if a service is declared in the manifest but its
* class was not included in the compiled code.
* @param serviceInfo The ServiceInfo of the service to check.
* @param userHandle The user under which to check for the service.
* @return {@code true} if the class exists, {@code false} otherwise.
*/
private boolean serviceClassExists(ServiceInfo serviceInfo, UserHandle userHandle) {
Log.i(this, "serviceClassExists check");
try {
Context packageContext = mContext.createPackageContextAsUser(
serviceInfo.packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY, userHandle);
ClassLoader classLoader = packageContext.getClassLoader();
Class.forName(serviceInfo.name, false, classLoader);
return true;
} catch (NameNotFoundException | ClassNotFoundException e) {
Log.w(this, "Skipping InCallService: class not found for " + serviceInfo.name);
return false;
} catch (Exception e) {
Log.e(this, e, "Error checking for existence of " + serviceInfo.name);
return false;
}
}
这是我见过的最令人难以置信的漏洞。该代码使用 Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY 从任意应用加载代码;虽然看起来有一些措施旨在防止任意代码执行,例如向 Class.forName() 传递 false 以防止类初始化,但应用仍然可以声明一个自定义的 AppComponentFactory,该工厂会在调用 getClassLoader() 时被调用。
另一方面,该 bug 存在于 InCallController.java 中,它是 com.android.server.telecom 包的一部分,而非 com.android.phone。值得注意的是,该包在 AndroidManifest.xml 中声明了 android:sharedUserId="android.uid.system" 和 android:process="system",因此它运行在 system_server 进程中,这是 Android 中权限最高的用户态进程之一。因此,我们现在能够在 system_server 内部执行任意 Java 代码。
令人惊讶的是,即使在 AI 时代,谷歌工程师也会犯下如此大的错误。我们于 2026 年 7 月 23 日发现并向 Android 安全团队报告了此问题。他们告诉我们这是一个重复报告。谷歌已将月度安全公告改为季度发布,这可能解释了为什么该漏洞在 Android 17 发布 3 个月后仍未修复。
该漏洞被分配为 CVE-2026-49881,并在 2026 年 9 月通过移除 serviceClassExists 逻辑以解决安全漏洞进行了修复。
第一个 bug 允许我们将权限提升至 system,但距离 root 仍然很远。完整的 root 至少需要 UID 0 并且不受 SELinux 的限制。
现在介绍内核 1-day 漏洞:DirtyFrag 漏洞。此处不再详述其基本原理;请参阅原始报告者的技术分析。有两个变体:CVE-2026-43500 需要 RxRPC,而 Android 通用内核已禁用它;CVE-2026-43284 需要 xfrm-ESP,可在 Android 上利用。然而,SELinux 禁止不受信任的应用使用该功能:
# Privileged netlink socket interfaces.
neverallow { appdomain -network_stack }
domain:{
netlink_tcpdiag_socket
netlink_nflog_socket
netlink_xfrm_socket
netlink_audit_socket
netlink_dnrt_socket
} *;
唯一允许的域是 system_server、network_stack 和 netd。为了利用 DirtyFrag,攻击者必须首先攻陷其中一个被列入白名单的特权进程。
将两个 bug 结合起来。虽然用户态 bug 允许我们在 system_server 内部执行 Java 代码,但 SELinux 也禁止 system_server 从 /data 加载原生库或映射匿名可执行内存。这使得无法使用原生代码,增加了利用难度。因此,最好在 com.android.networkstack 内部执行代码,该进程可以从我们的 APK 加载原生代码,并且拥有足够的权限来利用 DirtyFrag。
幸运的是,system_server 是 ActivityManager 运行的进程。ActivityManager 将每个应用进程的 IApplicationThread 句柄存储在一个 Java map 中,由于我们与 ActivityManager 运行在同一进程中,因此可以使用 Java 反射检索它们。这样,我们就可以向 com.android.networkstack 发送任意命令,强制其加载我们的代码。有关此技巧的更多信息,请参阅我之前针对 CVE-2026-0091 的利用。
DirtyFrag 允许覆盖只读文件。这在 Linux 世界中是一个强大的原语,因为我们可以覆盖具有 SUID 位的 su 二进制文件。然而,在 Android 世界中我们没有 su。我们参考 polygraphene 针对 DirtyPipe 的利用,将 DirtyFrag 转化为 Android 上的内核代码执行:
libc.so、libc++.so 和 /vendor/lib64/libstagefright_aidl_bufferpool2.so。libstagefright_aidl_bufferpool2.so 被标记为 vendor_file 域,因此无法从 network stack 进程访问。解决方案是先修补 /apex/com.android.runtime/bin/crash_dump64,执行它,一旦我们转换到 crash_dump 域,就可以打开 libstagefright_aidl_bufferpool2.so。init 进程中的代码执行。由于 libc++.so 已被修补,我们的代码以 UID 0 和 init 域执行。然后我们执行 /vendor/bin/modprobe 以转换到 vendor_modprobe 域。modprobe 被执行时,由于 也被修补,我们的代码在 域下执行。我们现在可以加载内核模块,但仅限于具有指定标签的文件。我们加载具有 标签的 。libc.sovendor_modprobevendor_filelibstagefright_aidl_bufferpool2.solibstagefright_aidl_bufferpool2.so,该文件的真实内容已被我们自己的内核模块替换。内核模块被加载,现在我们可以做任何事情,包括调整 SELinux 策略或将 SELinux 设置为 permissive。