18、Binder客户端漏洞:恶意客户端构造、binder_transaction_data伪造、服务端欺骗攻击
好,我们进入第18章。前面聊了很多服务端和驱动层的安全问题,今天换个角度——站在攻击者的立场,看看恶意客户端怎么利用Binder机制搞事情。
说白了,Binder通信不是单向的。服务端会收到客户端的请求,然后处理数据、返回结果。但如果客户端是恶意的呢?它完全可以构造畸形的binder_transaction_data,欺骗服务端执行非预期的操作。我在项目中就遇到过类似的攻击案例,嗯,今天把这些坑都给你拆开讲。
18.1 恶意客户端的基本思路
先理清攻击者的目标。恶意客户端想干什么?无非是:
- 绕过权限检查:让服务端以为自己有更高的权限
- 触发服务端崩溃:构造异常数据导致服务端进程挂掉
- 执行未授权操作:比如读写敏感文件、修改系统设置
- 提权:从普通App权限提升到system甚至root
你想想看,Binder驱动在kernel层,它只负责数据拷贝和线程管理,不负责业务逻辑的合法性校验。所以只要数据格式符合规范,驱动就会把它送到服务端。至于服务端怎么处理,那就是另一回事了。
核心观点:Binder驱动是“哑巴”传输层,它不关心数据内容是否恶意。攻击者只要构造合法的binder_transaction_data结构,就能把任意数据塞给服务端。
18.2 binder_transaction_data结构剖析
要伪造数据,先得搞清楚binder_transaction_data长什么样。这是Binder通信的“信封”,所有跨进程数据都装在里面。
struct binder_transaction_data {
/* 通用字段 */
union {
__u32 handle; // 服务端句柄
binder_uintptr_t ptr; // 内核内部使用
} target;
binder_uintptr_t cookie; // 服务端附加数据
__u32 code; // 事务码(比如TRANSACTION_TYPE)
/* 权限相关 */
__u32 flags; // 标志位,包含TF_ONE_WAY等
pid_t sender_pid; // 发送方PID
uid_t sender_euid; // 发送方有效UID
/* 数据区 */
binder_size_t data_size; // 数据大小
binder_size_t offsets_size; // 偏移数组大小
union {
struct {
binder_uintptr_t buffer; // 数据缓冲区
binder_uintptr_t offsets; // 偏移数组
} ptr;
__u8 buf[8]; // 小数据直接内联
} data;
};
注意看sender_pid和sender_euid这两个字段。它们是由Binder驱动自动填充的,客户端无法伪造。驱动在拷贝数据时会从当前进程的credential中读取这些值,然后写入结构体。
但是——其他字段呢?target.handle、code、flags、data.buffer、data.offsets……这些全是客户端可控的。尤其是data.buffer指向的数据内容,客户端想写什么就写什么。
避坑指南:我曾经见过一个开发者,他以为sender_pid是客户端传过来的,于是在服务端用这个值做权限校验。结果呢?攻击者根本改不了这个字段,但服务端却误以为可以信任它。其实正确的做法是用Binder.getCallingPid(),那个才是驱动层保证的。
18.3 伪造binder_transaction_data的常见手法
好,现在我们知道哪些字段可控了。攻击者具体怎么操作?我总结了几种典型手法:
18.3.1 伪造Binder对象引用
Binder通信中,客户端可以传递Binder对象给服务端。比如通过Parcel.writeStrongBinder()。攻击者可以伪造一个Binder引用,指向一个恶意服务端。
具体做法:
- 在
data.offsets中填入偏移量,指向flat_binder_object结构 - 在
flat_binder_object中设置type = BINDER_TYPE_HANDLE handle字段填上一个恶意服务端的句柄
服务端收到后,如果它信任这个Binder对象并调用它,就会连接到攻击者控制的恶意服务。这就是服务端欺骗攻击的雏形。
18.3.2 伪造文件描述符
Binder支持传递文件描述符(FD)。攻击者可以伪造一个FD,指向敏感文件(比如/data/system/packages.xml)。
构造方式:
- 在
flat_binder_object中设置type = BINDER_TYPE_FD handle字段填上目标文件的FD编号
但这里有个限制:驱动会校验FD是否属于当前进程。攻击者只能传递自己已经打开的FD。所以这个手法更多用于绕过服务端的FD权限检查——服务端以为FD来自一个可信进程,实际上却是攻击者打开的。
18.3.3 构造超大或超小数据
攻击者可以设置data_size为极大值(比如0xFFFFFFFF),或者极小值(比如0)。这会导致服务端在解析Parcel时出现内存越界或空指针解引用。
我记得有个经典案例:某系统服务在onTransact()中直接调用Parcel::unmarshall(),没有检查数据长度。攻击者传一个长度为0的数据包,服务端直接崩溃。嗯,这就是典型的拒绝服务攻击。
警告:不要以为服务端是系统进程就安全。很多系统服务(比如ActivityManagerService、PackageManagerService)在早期版本中都存在这类漏洞。攻击者可以让系统UI崩溃、让手机重启,甚至触发内核panic。
18.4 服务端欺骗攻击详解
这是本章的重点。什么叫服务端欺骗攻击?
正常情况下,客户端A请求服务端B的服务。攻击者C作为恶意客户端,构造一个请求发给B。B处理完返回结果给C。但攻击者C可以让B以为自己在跟A通信,从而获取本应只给A的数据或权限。
攻击流程:
- 攻击者C打开Binder设备,获取服务端B的句柄
- 构造
binder_transaction_data,其中target.handle指向B - 在数据区中伪造一个Binder对象,指向攻击者自己的服务
- 发送事务给B,B处理请求时,会调用这个伪造的Binder对象
- 攻击者C在自己的服务中接收回调,获取敏感数据
为什么能成功?因为Binder驱动只保证sender_pid和sender_euid的真实性,但不保证数据内容的真实性。服务端如果盲目信任数据中的Binder对象,就会中招。
真实案例:Android 4.x时代,MediaServer进程存在一个漏洞。攻击者可以构造一个恶意客户端,向MediaServer发送伪造的Binder对象。MediaServer在处理过程中会回调这个对象,攻击者就能获取到MediaServer的权限,进而访问摄像头、麦克风等硬件。这就是CVE-2015-6624,一个经典的提权漏洞。
18.5 防御思路与最佳实践
讲了这么多攻击手法,怎么防?我个人的经验是:
- 永远不要信任客户端传来的Binder对象:如果服务端需要回调客户端,应该使用
Binder.getCallingUid()验证对方身份,而不是直接调用传过来的Binder - 严格校验数据长度和格式:在
onTransact()中检查data.data_size和data.offsets_size是否合理 - 使用权限检查机制:在服务端入口处调用
checkCallingPermission(),确保调用方有相应权限 - 避免在Binder回调中执行敏感操作:如果必须回调,先验证回调对象的身份
个人经验:我建议在服务端实现一个白名单机制。只允许特定UID的客户端调用特定接口。对于回调场景,服务端应该自己维护一个回调列表,而不是信任客户端传来的任意Binder对象。这样做虽然麻烦一点,但能从根本上杜绝服务端欺骗攻击。
18.6 本章小结
恶意客户端攻击的核心,就是利用Binder驱动只做传输、不做校验的特点,构造畸形的binder_transaction_data。攻击者可以伪造Binder对象、文件描述符,或者构造异常数据触发服务端崩溃。
服务端欺骗攻击尤其危险——它能让攻击者冒充其他客户端获取敏感数据或提升权限。防御的关键在于:服务端不能盲目信任数据内容,必须结合驱动层提供的PID/UID信息做二次校验。
嗯,这一章的内容就到这里。记住一句话:Binder驱动是管道,不是警察。数据到了服务端,怎么用、信不信,全看服务端自己的实现。