从不受信任的应用获取OnePlus 15的root权限
摘要
安全研究人员发现OnePlus 15中存在两个漏洞,这些漏洞可串联起来允许不受信任的应用获取root权限;OnePlus已为许多设备修复此问题。
<p><a href="https://lobste.rs/s/a5rcxy/getting_root_on_oneplus_15_from_untrusted">评论</a></p>
查看缓存全文
缓存时间: 2026/09/29 18:03
# 从不受信任应用获取 OnePlus 15 root 权限:通过音频调试服务与厂商 HAL | nns.ee
来源:https://blog.nns.ee/2026/09/24/oneplus-root
我从 OnePlus 3T 起就开始使用 OnePlus 手机。目前我使用的是 OnePlus 15(CPH2747),运行 OxygenOS 16,已使用数月,整体体验相当满意。我决定稍微探究一下其固件内容,特别想了解纯净安装的固件中是否存在任何允许我从 Play Store 之外安装的普通应用最终以 root 权限运行的组件。结果确实存在!我发现了两个独立漏洞,可以组合形成一条相当简洁的利用链:`untrusted_app`(不受信任应用)-> uid 0(root),具备全部 Linux 能力权限,可从一个完全无需特殊权限的普通可安装 APK 实现。
关于方法的说明:我的 OP15 是日常主力机,因此在开发漏洞利用程序时,我不希望预先获取 root 或改变其状态。碰巧我手边有一台已解锁且因无关原因获取过 root 的旧款 OnePlus 12 Pro(CPH2581),我便在该设备上进行逆向工程和漏洞利用开发。一旦我制作出可运行的概念验证(PoC),便将完全相同且未修改的 APK 安装到我的 OP15 上,并且一次就成功了。因此,这篇分析主要针对 OP15,但大多数 Ghidra 截图和设备命令来自 OP12。OnePlus 随后确认该漏洞影响了多个软件版本下的众多 OnePlus 和 OPPO 设备,但尚未提供受影响设备或软件版本的完整列表。不过,我可以确认,至少在 OnePlus 15 上,这些问题已在版本 `16.0.10.500 (EX01)` 中修复。
**2026-09-28 更新**:OnePlus 安全团队已联系并确认,在仍在维护的设备中,有 151 款设备已收到补丁,另有 18 款待发布。完整更新如下(https://blog.nns.ee/2026/09/24/oneplus-root/#2026-09-28-update)。
### Android 沙箱机制快速入门 (https://blog.nns.ee/2026/09/24/oneplus-root#a-quick-primer-on-android-sandboxing)
在 Android 上,一个正常安装的应用(无论来自 Play Store、侧载等)运行在一个称为 `untrusted_app`(不受信任应用)的 SELinux 域中。其限制相当严格——它只能与少数系统 Binder 服务通信,读取自身的数据目录,若获得权限则可使用摄像头等,除此之外功能有限。本地权限提升的常见目标就是突破 `untrusted_app`,进入具有 uid 0(root)和更宽松 SELinux 域的位置。
Android 是一个复杂的系统,存在*大量*系统 Binder 服务,每个都是潜在的攻击面。OxygenOS 在 AOSP 服务之上增加了自己的服务。我认为 AOSP 服务很可能比 OxygenOS 服务受到过更多关注,因此我将主要精力放在了这部分。
### 漏洞 1:AtlasService 允许任何应用以 root 身份运行命令 (https://blog.nns.ee/2026/09/24/oneplus-root#bug-1-atlasservice-lets-any-app-run-commands-as-root)
AtlasService 是 OxygenOS 的一项功能,据我所知,用于收集遥测和调试事件。它以 root 身份运行,并接受来自任何进程的 Binder 调用。我不完全确定 "Atlas" 具体指什么,该进程本身称为 `atlasservice`,相关库是 `libatlasservice.so`。我未能找到太多相关文档。
查看 `BnAtlasService::onTransact`,有几个事务代码。有趣的是代码 `2`,对应 `setEvent(String8 name, String8 value)`。对调用者没有任何权限检查——任何 UID 都可以调用它。
`setEvent` 通过一系列注册的监听器分发事件。大多数监听器执行的是常规操作——记录日志、上传数据等——但其中一个监听器 `OplusAtlasLogWriter::handleEvent` 对事件名进行字符串比较,匹配 `atlas_event_multimedia_audio_dumpsys`。如果事件名匹配,它会调用 `dumpsysAudioInfo`,该函数启动一个工作线程执行以下操作:
```
property_set("oplus.audio.dumpinfo.type", value); // 攻击者控制
property_set("ctl.start", "audiodumpinfo");
```
`ctl.start=audiodumpinfo` 告知 `init` 启动一个名为 `audiodumpinfo` 的服务。查看 `/system_ext/etc/init/audiodumpinfo.rc`:
```
service audiodumpinfo /system_ext/bin/audioDumpInfo
class main
user root
group root system everybody sdcard_rw
seclabel u:r:dumpstate:s0
disabled
oneshot
```
因此 `init` 以 uid 0 和 `dumpstate` SELinux 域启动 `/system_ext/bin/audioDumpInfo`。那么 `audioDumpInfo` 如何处理我们攻击者控制的属性值呢?它调用 `GetProperty("oplus.audio.dumpinfo.type")` 并将结果直接放入字符串缓冲区:
```
/data/persist_log/TMP/audio_dumpsys/<我们控制的值>/
```
然后它逐级遍历路径,对于每个尚不存在的组件,创建目录并执行 `system("chmod 777 " + prefix)`。我想你已经看出问题所在了。我们提供的值未经转义,直接作为 shell 命令传入 `system()` 函数。我们只需注入 `;`、某些命令以及 `#` 注释掉剩余部分。Android 属性值上限为 92 字节。这对于完整命令来说相当紧张,但对于 `sh&1|log -t AtlasOut;#` 则完全足够。
这意味着任何应用都可以基本上以 uid 0 身份运行命令。SELinux 上下文最终为 `u:r:dumpstate:0`,虽然这不是完全无限制的 root(dumpstate 的策略相当严格),但它具有 uid 0,能够读写大部分 `/data`,并且可以调用众多信任 uid-0 调用者的厂商 Binder 服务。这引出了第二个漏洞。
### 漏洞 2:olc2 HAL 的 `doShell` 字面意义上为你运行 shell (https://blog.nns.ee/2026/09/24/oneplus-root#bug-2-olc2-hal-doshell-literally-runs-a-shell-for-you)
OxygenOS 搭载了一个名为 `vendor.oplus.hardware.olc2.IOplusLogCore/default` 的厂商 HAL,运行于 `/odm/bin/hw/vendor.oplus.hardware.olc2-V3-service`,并暴露一个 Binder 接口。该服务在事务代码 `6` 处有一个名为 `doShell(String cmd)` 的方法。查看其实现确实是“我也不知道我期待什么”的时刻。Ghidra 对 OlcHwService::doShell 的反编译显示 uid 检查和到 /vendor/bin/sh 的 execl 调用 (https://blog.nns.ee/img/2026-09-24-oneplus-root/olc-doshell.png):
```
if (getCallingUid() == 0) {
__android_log_print(3, "OLC_HAL", "olc doShell(%s) called", cmd);
pid = fork();
if (pid == 0) {
execl("/vendor/bin/sh", "sh", "-c", cmd, NULL); // ...
}
}
```
唯一的门槛是 `getCallingUid() == 0`。如果你是 uid 0,它会为你运行任意 shell 命令。代码 `7` 处还有一个 `doShellBlocking`,功能相同,但如果你需要退出状态,它会使用 `waitpid()`。
使其对我们有趣且有用的部分是子进程继承的 SELinux 域。当 `init` 启动 olc2 HAL 服务时,其 SELinux 类型为 `hal_oplus_olc_aidl_default`。然后策略中的 `type_transition` 规则将该 HAL 的执行子进程送入 `vendor_qti_init_shell`:
```
type_transition hal_oplus_olc_aidl_default vendor_qti_init_shell_exec:process vendor_qti_init_shell
```
而 `vendor_qti_init_shell` 的 `CapBnd = 0x1ffffffffff`,意味着它拥有*所有*能力权限。`CAP_SYS_MODULE`、`CAP_SYS_RAWIO`、`CAP_SYS_PTRACE` 等。这……比 dumpstate 多得多。Dumpstate 的能力集更具限制性,无法加载内核模块等操作。就 Linux CAPs 而言,`vendor_qti_init_shell` 本质上是一个无限制的 shell,仅受 SELinux 约束。
### 组合利用 (https://blog.nns.ee/2026/09/24/oneplus-root#chaining-them)
这两个漏洞可以完美组合:
1. 不受信任应用通过 Binder 调用 `AtlasService.setEvent("atlas_event_multimedia_audio_dumpsys", "")`。
2. AtlasService 设置属性 `oplus.audio.dumpinfo.type` 并触发 `ctl.start=audiodumpinfo`。
3. `init` 以 `u:r:dumpstate:s0` 启动 `/system_ext/bin/audioDumpInfo`。
4. `audioDumpInfo` 读取我们的属性,使用它调用 `system()`,我们以 `dumpstate` 身份获得 shell。
5. 仍以 `dumpstate` 运行,我们通过 Binder 调用 olc2 HAL 的 `doShell(cmd)`。
6. olc2 HAL fork/exec `/vendor/bin/sh -c cmd`,最终进入 `u:r:vendor_qti_init_shell:s0`。
策略将 `dumpstate` 放入 `hal_oplus_olc_aidl_client` 属性中,该属性被明确允许调用 `hal_oplus_olc_aidl_server`:
```
$ sesearch -A -s dumpstate -c binder sepolicy_clean.bin | grep olc
allow hal_oplus_olc_aidl_client hal_oplus_olc_aidl_server:binder { call transfer };
```
……以及一长串其他 HALs(`debuglog`、`hal_camera_default`、`hal_charger_oplus` 等),因此 HAL 端的 `getCallingUid() == 0` 检查是*唯一*的权限门槛。任何在这些包含属性域中的 uid-0 进程都可以使用它。
### 构建概念验证 (https://blog.nns.ee/2026/09/24/oneplus-root#building-the-poc)
PoC 是一个常规 Android 应用,在清单中无需特殊权限,目标 API 为 35。该应用在 `MainActivity.onCreate` 中执行以下操作:
1. 将一个小型 `boot.sh` 写入 `getExternalFilesDir()`,该脚本使用我们的类执行 `app_process`。
2. 将 `classes.dex` 从我们自己的 APK 解压到同一目录。
3. 使用执行 `sh` 的载荷调用 `AtlasService.setEvent`,其中 `` 指向我们已安装的 APK。
结果证明,这*不*起作用。已安装的 APK 标记为 `apk_data_file`,而 `dumpstate` 无法读取该标签。但应用的外部文件目录标记为 `media_rw_data_file`/fuse,dumpstate*可以*读取。因此我们将 `classes.dex` 从自己的 APK 中解压(实际上就是解压缩)并放置在那里。`boot.sh` 最终如下所示:
```
#!/system/bin/sh
export CLASSPATH=/sdcard/Android/data/com.research.poc/files/classes.dex
exec /system/bin/app_process /system/bin com.research.poc.Pwn olc /sdcard/Android/data/com.research.poc/files/cmd.txt
```
`app_process` 是在 Android 上启动 JVM 托管进程的二进制文件(zygote 也使用它)。通过设置 `CLASSPATH` 环境变量,我们可以从任何想要的 .dex 文件运行一个类。
`Pwn.main` 以 `dumpstate` 身份运行,并执行对 `olc2.doShell(cmd)` 的 Binder 调用。这时我遇到了一个特殊情况。
### Java 的 `writeInterfaceToken` 与厂商 AIDL HAL (https://blog.nns.ee/2026/09/24/oneplus-root#java-writeinterfacetoken-against-a-vendor-aidl-hal)
olc2 HAL 是使用 AIDL NDK C++ 绑定编写的。从 Java 调用 AIDL 厂商 HAL 的官方方式……实际上并不存在。你根本不应该从应用进程与厂商 HAL 通信。但 `dumpstate` 是系统进程,所以*它*可以,如果你能弄清楚正确的线上格式。
我以为这会很难。厂商 Binder 服务通常有严格的接口令牌版本控制,并且存在一个与常规 `android.os.IBinder`/`ServiceManager.getService` 路径分离的 `android.os.IHwBinder` 路径,用于与 HIDL 厂商 HAL 通信。由于这是 AIDL 而非 HIDL,我不确定自己处于哪一边。或者,更确切地说,我以为我不确定。出于直觉,我尝试了 `ServiceManager.getService("vendor.oplus.hardware.olc2.IOplusLogCore/default")`,用 `Parcel.writeInterfaceToken("vendor.oplus.hardware.olc2.IOplusLogCore")` 写入接口令牌,写入字符串参数,`binder.transact(6, data, reply, 0)`。它竟然直接成功了。
```
IBinder b = ServiceManager.getService("vendor.oplus.hardware.olc2.IOplusLogCore/default");
Parcel data = Parcel.obtain(), reply = Parcel.obtain();
data.writeInterfaceToken("vendor.oplus.hardware.olc2.IOplusLogCore");
data.writeString(cmd);
b.transact(6, data, reply, 0);
```
还有一个关于 `Parcel.writeString8` 与 `writeString` 的细微差别。稳定的 AIDL 以 UTF-16(`String16` 线上格式)传输字符串,而 Java 的 `Parcel.writeString` 也发射这种格式——因此对于 olc2 调用,`writeString` 直接有效。另一方面,AtlasService 使用较旧的 `String8` 线上格式。线上的 `String8` 格式为 `int32 长度, UTF-8 字节, '\0', 填充至4字节`。即使在解除隐藏 API 限制后,Java 的 `Parcel` 在 API 35 上并未直接暴露此格式,因此我最终手动模拟:`writeInt(len)`,然后使用辅助 `Parcel` 的 `writeByteArray(bytes + '\0')`,再从辅助 Parcel 的自身长度前缀之后开始,对主 parcel 使用 `appendFrom`。有些笨拙但确实有效。
### 在 OP15 上测试 (https://blog.nns.ee/2026/09/24/oneplus-root#testing-on-the-op15)
在 OP12 上一切就绪后,我将同一未修改的 APK 安装到我的 OP15(CPH2747,OxygenOS 16.0.3.503,安全补丁级别 2026-02-01,内核 6.12.23)上尝试:
概念验证应用命令输出显示权限提升 (https://blog.nns.ee/img/2026-09-24-oneplus-root/poc.png)
一次成功。鉴于两款不同的 OnePlus 机型在不同的内核分支上都存在此问题,我将其视为 OxygenOS 16 的普遍问题,而非特定于某款手机。
### 修复方案 (https://blog.nns.ee/2026/09/24/oneplus-root#remediation)
如果由我来修复,对于 AtlasService,我会(或更确切地说,两者都做):
1. 对 `AtlasService::setEvent` 应用调用者 UID / SELinux 对等体检查。
2. 停止将未经过清理的用户数据传入 `audioDumpInfo` 中的 `system()` 调用。在一个可通过启动触发的服务中执行 `system("chmod 777 " + untrusted_input)` 在 2026 年绝对是个“选择”。
对于 olc2,修复方法要么完全移除整个 `doShell` 方法(这东西到底为什么存在?),要么至少添加 SELinux 对等体过滤器,使得只有非常特定的调试守护进程可以访问它,而不是任何具有 uid 0 的进程。
---
## 其他事项 (https://blog.nns.ee/2026/09/24/oneplus-root#miscellaneous)
我在过程中遇到的一些事情,不够有趣放入主线故事,但我想记录下来。
### 逆向 AtlasService 事件分发 (https://blog.nns.ee/2026/09/24/oneplus-root#reversing-the-atlasservice-event-dispatch)
`libatlasservice.so::OplusAtlasLogWriter::handleEvent` 对硬编码字符串进行了多次 `memcmp` 调用。字符串字面量并非存储为单个 `const char*`;而是编码为一对 64 位四字和一个 32 位双字,作为立即数加载。Ghidra 并未将这些显示为明显的字符串比较——你会看到 `if (*(long*)v == 0x5f73616c7461 && ...)` 并且必须手动反转字节序。用于转储它们的小型脚本:
```
import struct
def qw(x):
return struct.pack("<Q", x)
def dw(x):
return struct.pack("<I", x)
# ... 解码逻辑
```
在这个服务上我看到了三种代码(1, 2, 3),在确定代码 2 读取两个 `String8` 参数之前,我尝试了所有代码。代码 1 是 `registerNativeClient`,它本身又是一个麻烦(它允许你注册一个回调 Binder,服务会在 root 下调用它,这可能是一个单独的漏洞,但我尚未深入研究)。
---
## 2026-09-28 更新 (https://blog.nns.ee/2026/09/24/oneplus-root#2026-09-28-update)
OnePlus 已联系并分享了当前修复状态的更多细节。引用如下:
> 我们已与负责团队确认,该漏洞已在新版本中修复。相关代码已于七月底合并。此审查覆盖仍在维护中的 OPPO/realme/OnePlus 出口型号:151 款型号已修复,18 款待发布。所有待发布型号计划于十月发布。这些现有维护分支的发布周期为两到三个月。尽管修复已于七月底合并,但只能在维护窗口期内通过 OTA 推送,因此存在一段更新仍待发布的短暂时期。
>
> 修复后的构建版本可如下识别:
> - 在 16.1.0 系列,修复构建始于 16.0.10.500。当最后一位数字为 500 或更大时,构建版本已修复。
> - 在 16.0.0 系列,修复构建始于 16.0.5.1200。当最后一位数字为 1,200 或更大时,构建版本已修复。
> - 在 15.0.0 系列,修复构建始于 15.0.0.2000。当最后一位数字为 2,000 或更大时,构建版本已修复。
---
## 时间线 (https://blog.nns.ee/2026/09/24/oneplus-root#timeline)
相似文章
OEM灾难:无特权Android应用在三星、小米等设备上获取root权限
该研究介绍了一种通用的漏洞利用策略,该策略利用Android内核驱动中的OEM特定漏洞,在三星、小米等设备上实现root权限,重点在于可靠性、可移植性和通用性。
从Firefox提升权限至Android Root
本文宣布了Android 17上首个从浏览器到内核的全链路远程代码执行漏洞,源代码即将发布。
Pixel 10 的零点击利用链
谷歌 Project Zero 发布了针对 Pixel 10 的零点击利用链,利用 Dolby 漏洞和新 VPU 驱动缺陷实现 Android 根权限获取。
AI发现了一个被所有人忽略了15年的Linux根权限漏洞
来自Nebula Security的AI工具VEGA发现了Linux内核中一个存在15年之久的释放后使用漏洞(GhostLock),该漏洞允许任何登录用户获得root权限。该漏洞自2011年存在,已于4月修补,但部署进度不均衡。
@0x0SojalSec:如果你已root你的安卓(或打算root),一份涵盖500+款设备的root教程与模块的完整清单汇聚一处。太棒了…
一条推特帖子汇总了500+款安卓设备的root教程与模块,涵盖Magisk、KernelSU、隐私工具、游戏优化等。