Android移动安全第三章_Binder服务安全
移动安全
前两章讲了组件导出和 Intent,都是应用层的攻击面。这一章往下走一层——Binder。它是 Android 所有跨进程通信的底层基础,四大组件的交互、系统服务的调用,底下跑的都是 Binder。理解它,才能理解系统服务为什么会被未授权调用。
> 系列目录: > > 1. Android 组件导出安全 > 2. Android Intent 安全 > 3. Android Binder 服务安全(本章) > 4. Android ContentProvider 安全 > 5. Android WebView 安全 > 6. Android UI 欺骗与钓鱼 > 7. Android Deep Link 安全 > 8. Android 广播安全 > 9. Android PendingIntent 安全 > 10. Android 系统设置安全 > 11. Android SSRF 与网络安全 > 12. Android 加密与数据存储安全 > 13. Android 认证与证书校验 > 14. Android Zip Slip 路径遍历 > 15. Android Fragment Injection > 16. Android SELinux 与沙箱机制 - - - - - - 1. 前言 ----- Android 是一个多进程系统。每个 App 运行在自己的进程中,系统服务运行在 system\_server 进程中。system\_server 是 Android 中权限最高的用户态进程(UID 1000),ActivityManagerService、PackageManagerService 等系统服务都在这个进程里。进程之间的内存是隔离的,不能直接访问彼此的数据。 那 App 怎么调用系统功能?比如安装应用、获取位置、读取通知?答案是 Binder。 Binder 是 Android 的跨进程通信(IPC)机制。它不是 Linux 原生的,而是 Android 在内核中加入的一个驱动(/dev/binder),专门为 Android 的安全模型设计。App 想调用系统服务,就通过 Binder 发起一次跨进程调用,内核负责把请求从调用方进程传递到服务方进程,同时携带调用者的 UID(User ID,Android 为每个 App 分配的唯一用户标识)和 PID(Process ID,进程标识)信息。 从安全角度看,Binder 有两个特性: - 调用者身份不可伪造。UID 和 PID 由内核填入,应用层无法篡改 - 权限检查是服务端的责任。内核只负责传递身份信息,检不检查、怎么检查,完全由服务端代码决定 第一个特性是安全的基础,第二个特性是漏洞的来源。如果服务端忘了检查调用者身份,任何 App 都能调用它的方法。 - - - - - - 2. Binder 通信机制 -------------- ### 2.1 整体架构 Binder 通信涉及四个角色: | 角色 | 说明 | 典型实例 | |---|---|---| | Client | 发起调用的一方 | 普通 App | | Server | 提供服务的一方 | system\_server 中的系统服务 | | ServiceManager | 服务注册表,负责服务的注册和查找 | servicemanager 进程 | | Binder 驱动 | 内核模块,负责跨进程数据传输 | /dev/binder | 一次典型的调用流程如下图所示:  1. Server 启动时,通过 `ServiceManager.addService()` 注册服务 2. Client 通过 `ServiceManager.getService()` 获取服务的 Binder 代理对象 3. Client 调用代理对象的方法,Binder 驱动将请求(包括方法编号、参数、调用者 UID/PID)传递给 Server 4. Server 处理请求并返回结果,Binder 驱动将结果传回 Client ServiceManager 本身也是一个 Binder 服务,但它的 handle 固定为 0,所有进程都能直接访问。它相当于一个电话簿——不需要知道系统服务的进程地址,只需要知道服务名,就能通过 ServiceManager 查到对应的 Binder 引用。 ### 2.2 AIDL 与 transaction code 开发者通常不直接操作 Binder 驱动,而是通过 AIDL(Android Interface Definition Language,Android 接口定义语言)定义跨进程调用的接口。AIDL 类似于定义一个远程可调用的 Java 接口,编译器会自动生成两部分代码:Stub(服务端骨架,负责接收和分发请求)和 Proxy(客户端代理,负责封装和发送请求)。 一个简单的 AIDL 接口: ```aidl // IDeviceManager.aidl interface IDeviceManager { String getDeviceId(); // transaction code = 1 void setDeviceName(String n); // transaction code = 2 boolean resetDevice(); // transaction code = 3 } ``` 每个方法会被分配一个 transaction code(事务码),从 `FIRST_CALL_TRANSACTION`(值为 1)开始递增。Client 调用方法时,实际上是向 Binder 驱动发送一个包含 transaction code 和序列化参数的数据包。 这意味着即使没有 AIDL 文件,只要知道 transaction code 和参数格式,就可以通过 `service call` 命令直接调用服务方法: ```bash # service call <服务名> <transaction_code> [参数类型 参数值 ...] # 参数类型:i32 = 整数, s16 = 字符串 adb shell service call clipboard 1 ``` ### 2.3 Parcel:数据序列化 Binder 传输的数据通过 Parcel(包裹)对象序列化。Parcel 是一个二进制缓冲区,数据按顺序写入和读取: ```java // 客户端发送(Proxy 自动生成的代码) Parcel data = Parcel.obtain(); data.writeInterfaceToken("com.example.IDeviceManager"); // 接口描述符 data.writeString("new_device_name"); // 方法参数 mRemote.transact(2, data, reply, 0); // transaction code = 2 // 服务端接收(Stub 自动生成的代码) @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { switch (code) { case 2: { // setDeviceName data.enforceInterface("com.example.IDeviceManager"); String name = data.readString(); this.setDeviceName(name); reply.writeNoException(); return true; } } return super.onTransact(code, data, reply, flags); } ``` `enforceInterface()` 会校验接口描述符是否匹配,防止把发给 A 服务的请求误发给 B 服务。但它不是安全检查——不会验证调用者身份。 - - - - - - 3. 系统服务的注册与发现 ------------- ### 3.1 AOSP 原生服务 Android 系统自带大量 Binder 服务,运行在 system\_server 进程中。通过 `service list` 可以查看当前设备上所有注册的服务: ```bash adb shell service list ``` 输出(节选): ```php Found 287 services: 0 sip: [android.net.sip.ISipService] 1 phone: [com.android.internal.telephony.ITelephony] 2 isms: [com.android.internal.telephony.ISms] ... 50 activity: [android.app.IActivityManager] 51 package: [android.content.pm.IPackageManager] 52 clipboard: [android.content.IClipboard] ... ``` 方括号中的是 AIDL 接口名,前面的字符串是服务名,也就是 `getService()` 的参数。 ### 3.2 厂商自定义服务 厂商定制 ROM 会在 AOSP(Android Open Source Project,Android 开源项目)基础上添加自己的系统服务。这些服务通常以厂商前缀命名: ```bash adb shell service list | grep -i "miui\|xiaomi" ``` 可能看到类似: ```php miui.mqsas.IMQSNative miui.security.ISecurityManager xiaomi.IGameBoosterService ``` 这些服务和 AOSP 原生服务一样运行在 system\_server 中,拥有系统级权限。但它们不在 AOSP 的代码审查范围内,安全质量参差不齐。 ### 3.3 服务注册方式 系统服务的注册通常在 system\_server 启动过程中完成: ```java // SystemServer.java 中的典型注册方式 ServiceManager.addService("device_manager", new DeviceManagerService(context)); ``` 注册后,任何进程都可以通过 ServiceManager 获取该服务的 Binder 代理: ```java IBinder binder = ServiceManager.getService("device_manager"); IDeviceManager manager = IDeviceManager.Stub.asInterface(binder); manager.getDeviceId(); // 跨进程调用 ``` 这里没有任何权限检查。`ServiceManager.getService()` 对所有进程开放,获取 Binder 代理不需要任何权限。安全检查只能发生在服务端的方法实现中。 - - - - - - 4. 权限检查机制 --------- ### 4.1 调用者身份获取 Binder 驱动在传递调用请求时,会在内核层面记录调用者的 UID 和 PID。服务端通过以下 API 获取: ```java int callingUid = Binder.getCallingUid(); // 调用者的 UID int callingPid = Binder.getCallingPid(); // 调用者的 PID ``` 这两个值由内核填入,应用层无法伪造。 但有一个容易踩坑的地方:`Binder.getCallingUid()` 返回的是最近一次 Binder 调用的调用者 UID。如果服务端在处理请求的过程中又发起了另一个 Binder 调用(比如调用另一个系统服务),那么 `getCallingUid()` 的返回值会被覆盖为当前进程自己的 UID。 为了解决这个问题,Android 提供了 `clearCallingIdentity()` 和 `restoreCallingIdentity()`: ```java @Override public void sensitiveMethod() { // 先保存调用者身份 int callingUid = Binder.getCallingUid(); // 检查权限 if (checkPermission(callingUid) != GRANTED) { throw new SecurityException("Permission denied"); } // 清除调用者身份(后续 Binder 调用将使用自己的身份) long token = Binder.clearCallingIdentity(); try { // 执行需要系统权限的操作 doInternalWork(); } finally { // 恢复调用者身份 Binder.restoreCallingIdentity(token); } } ``` 如果开发者在检查权限之前就调用了 `clearCallingIdentity()`,那 `getCallingUid()` 返回的就是 system\_server 自己的 UID(1000),权限检查就形同虚设了。 ### 4.2 常见的权限检查方式 ```java // 方式 1:检查调用者是否持有特定 Android 权限 context.enforceCallingPermission( "android.permission.MANAGE_USERS", "Caller must hold MANAGE_USERS permission" ); // 方式 2:检查 UID 范围 int uid = Binder.getCallingUid(); if (uid >= Process.FIRST_APPLICATION_UID) { // >= 10000 是第三方 App throw new SecurityException("System only"); } // 方式 3:检查是否是特定的系统 App int uid = Binder.getCallingUid(); String[] packages = context.getPackageManager().getPackagesForUid(uid); if (!Arrays.asList(packages).contains("com.android.settings")) { throw new SecurityException("Only Settings app allowed"); } // 方式 4:检查调用者签名 PackageInfo info = pm.getPackageInfo(callingPackage, PackageManager.GET_SIGNATURES); // 比对签名是否是平台签名 ``` ### 4.3 缺失权限检查的后果 如果一个系统服务的方法没有做任何权限检查: ```java // 运行在 system_server 中(UID 1000) public class VulnSystemService extends IVulnService.Stub { @Override public String readSystemConfig(String path) { // 没有检查调用者身份 // 直接以 system 权限读取文件 return FileUtils.readFileToString(new File(path)); } } ``` 任何第三方 App 都可以调用这个方法,以 system\_server 的权限读取系统文件。这就是一个权限提升漏洞——普通 App 通过未授权的 Binder 调用获得了系统级文件读取能力。 - - - - - - 5. 攻击面分析 -------- ### 5.1 未授权方法调用 这是 Binder 服务最常见的漏洞类型。服务注册到 ServiceManager 后,所有 App 都能获取其代理对象。如果某个方法内部没有权限检查,就是一个可利用的攻击点。 通过 `service call` 可以直接测试: ```bash # 调用 clipboard 服务的第 1 个方法 adb shell service call clipboard 1 # 调用某个厂商服务的方法 adb shell service call miui.security 1 ``` 如果返回了正常数据而不是 SecurityException,说明该方法可能缺少权限检查。 `service call` 直接操作 Parcel 层,需要知道参数的精确格式。对于复杂参数(嵌套对象、数组等),手动构造 Parcel 数据比较困难。另一种方式是通过 Java 反射调用: ```java // 通过反射获取 ServiceManager(隐藏 API,需要反射访问) IBinder binder = (IBinder) Class.forName("android.os.ServiceManager") .getMethod("getService", String.class) .invoke(null, "target_service"); // 获取 AIDL 接口 Class<?> stubClass = Class.forName("com.vendor.ITargetService$Stub"); Object service = stubClass.getMethod("asInterface", IBinder.class) .invoke(null, binder); // 调用目标方法 Method method = service.getClass().getMethod("sensitiveMethod", String.class); Object result = method.invoke(service, "parameter"); ``` 也可以不依赖 AIDL 接口,直接构造 Parcel 数据发送: ```java Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); try { data.writeInterfaceToken("android.content.IClipboard"); data.writeString("com.test.app"); binder.transact(2, data, reply, 0); // code 2 = getPrimaryClip reply.readException(); // 读取返回数据... } finally { data.recycle(); reply.recycle(); } ``` ### 5.2 transaction code 枚举 如果没有 AIDL 文件,可以通过枚举 transaction code 来探测服务暴露了哪些方法: ```bash for i in $(seq 1 20); do echo "--- code $i ---" adb shell service call target_service $i done ``` 返回值的含义: | 返回 | 含义 | |---|---| | `Result: Parcel(...)` 有数据 | 方法存在且执行成功 | | `Result: Parcel(00000000 ...)` 全零 | 方法存在,返回空或 void | | 异常信息 | 方法存在但权限不足或参数错误 | | `Failed transaction` | 该 transaction code 不存在 | 配合反编译服务端代码(从 framework JAR 或 system APK 中提取),可以还原出每个 transaction code 对应的方法名和参数类型。 ### 5.3 Binder 代理泄露 有些场景下,系统服务会通过回调把内部 Binder 对象传递出来。如果这个 Binder 对象本不应该暴露给第三方 App,就构成了代理泄露。 ```java // 系统服务内部 public void registerCallback(ICallback callback) { // 把内部管理器的 Binder 引用通过回调传出去 callback.onReady(mInternalManager.asBinder()); } ``` 攻击者注册回调后,就获得了 `mInternalManager` 的 Binder 代理,可以直接调用它的方法,绕过了正常的访问路径。 ### 5.4 Parcel 反序列化漏洞 Binder 传输的数据通过 Parcel 序列化。如果服务端在反序列化时处理不当,可能导致安全问题。 Android 中的 Bundle(一种键值对容器,常用于 Intent extras 和 Binder 调用参数)有一个特性叫延迟反序列化(lazy unmarshalling)——Bundle 收到 Parcel 数据后不会立即解析所有内容,而是在第一次 get 操作时才真正反序列化: ```java @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { Bundle bundle = data.readBundle(); // 此时 Bundle 内部数据还没有被完全解析 String value = bundle.getString("key"); // 这里才触发反序列化 // 如果 Bundle 中包含恶意构造的 Parcelable 对象, // 反序列化过程可能触发非预期的行为 } ``` 这个特性曾经是多个 Android 提权漏洞的根源。攻击者在 Bundle 中放入精心构造的 Parcelable 对象,当系统服务反序列化时触发类型混淆(写入时是 A 类型,读取时被解释为 B 类型),最终实现权限提升。CVE-2017-13288 等 "Bundle 风水" 系列漏洞就属于这一类。 ### 5.5 dumpsys 信息泄露 `dumpsys` 命令可以获取系统服务的内部状态信息: ```bash # 查看某个服务的 dump 信息 adb shell dumpsys <服务名> # 例:查看 activity 服务的信息 adb shell dumpsys activity activities # 例:查看 package 服务中某个包的信息 adb shell dumpsys package com.target.app ``` 不是所有服务都支持 dump,dump 输出的内容由服务端的 `dump()` 方法决定。有些服务的 dump 会输出 token、session 信息、内部状态等敏感数据,这本身也可能是一个信息泄露点。 - - - - - - 6. 版本演进 ------- ### 6.1 SELinux 对 Binder 的限制 从 Android 5.0 开始,SELinux(Security-Enhanced Linux,一种内核级的强制访问控制机制,在标准的 Linux 权限模型之上增加了一层策略控制)被设为强制模式。SELinux 策略可以限制哪些进程能与哪些 Binder 服务通信: ```php # SELinux 策略示例 # 允许 platform_app 域访问 clipboard_service allow platform_app clipboard_service:service_manager find; # 禁止 untrusted_app 域访问某个厂商服务 neverallow untrusted_app vendor_custom_service:service_manager find; ``` 即使服务端代码没有权限检查,SELinux 也可能在更底层阻止调用。但 SELinux 策略的覆盖范围取决于厂商的配置——有些厂商自定义服务可能没有被 SELinux 策略覆盖到。 ### 6.2 Treble 架构与 Binder 域分离 Android 8.0 引入了 Treble 架构,将 Binder 分成了三个独立的域: - `/dev/binder`:framework 进程之间的通信(App ↔ system\_server) - `/dev/hwbinder`:framework 与 HAL(Hardware Abstraction Layer,硬件抽象层,负责连接 Android 框架和底层硬件驱动)之间的通信 - `/dev/vndbinder`:vendor(厂商)进程之间的通信 Android 10 进一步引入了 `/dev/vndbinder`,隔离 vendor 和 framework 的 Binder 通信。这种分离减少了跨域攻击的可能性——第三方 App 只能访问 `/dev/binder` 上的服务,无法直接与 HAL 服务通信。 ### 6.3 隐藏 API 限制 从 Android 9 开始,Google 限制了对隐藏 API(标记为 `@hide` 的系统 API,不在公开 SDK 中)的反射调用。ServiceManager 本身就是隐藏 API,直接反射调用会收到警告或被阻止: ```php Accessing hidden method Landroid/os/ServiceManager;->getService(Ljava/lang/String;)Landroid/os/IBinder; (greylist, reflection, allowed) ``` Android 对隐藏 API 按限制力度分了几个级别: | 列表 | 限制 | |---|---| | whitelist | 无限制 | | greylist | 允许但会打印警告 | | greylist-max-o | targetSdk > 26 时阻止 | | greylist-max-p | targetSdk > 28 时阻止 | | blacklist | 完全阻止 | 但这个限制可以被绕过(双重反射、JNI 调用等),所以它增加了攻击成本,但不构成安全边界。 ### 6.4 版本变化汇总 | 版本 | 变化 | |---|---| | Android 5.0 | SELinux 强制模式,Binder 调用受策略约束 | | Android 8.0 | Treble 架构,HAL 服务使用独立的 /dev/hwbinder | | Android 9 | 隐藏 API 限制,反射调用 ServiceManager 受到约束 | | Android 10 | /dev/vndbinder 隔离 vendor 和 framework 通信 | | Android 11+ | Stable AIDL 用于跨分区接口,增强接口稳定性 | - - - - - - 7. 总结 ----- Binder 是 Android IPC 的底层实现,所有系统服务都建立在它之上。 回顾一下: - Binder 驱动在内核层记录调用者 UID/PID,身份不可伪造,但权限检查是服务端的责任 - ServiceManager 是公开的服务注册表,任何 App 都能查询和获取服务代理 - 厂商自定义系统服务运行在 system\_server 中拥有系统权限,但不在 AOSP 的审查范围内 - `service call` 和反射调用是与 Binder 服务交互的两种方式 - SELinux、Treble 架构、隐藏 API 限制是系统层面的缓解措施,但都有各自的局限 下一章讲 Android ContentProvider 安全。ContentProvider 是四大组件中专门负责数据共享的,它的 query、openFile、call 方法各有不同的攻击面,SQL 注入和路径遍历是其中比较经典的漏洞模式。
发表于 2026-04-03 09:00:00
阅读 ( 955 )
分类:
漏洞分析
1 推荐
收藏
0 条评论
bReaK_1
7 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!