CVE--属于BlueBorne漏洞集。导致该漏洞的原因是AndroidBlueDroid和Fluoride蓝牙协议栈实现的SDPserver没有正确处理continuationstate。攻击者可以利用它泄露Android栈上的内存信息,然后绕过ASLR。
本文将基于标签为android-8.0.0_r1的AOSPFluoride蓝牙协议栈分析该漏洞。
背景知识
SDP简介
SDP(ServiceDiscoveryProtocol)是经典蓝牙中的高层协议,使用C/S架构。它定义了client如何发现server提供的服务:
上图中的关键点是,client必须等待server响应当前的requestPDU后,才能发送下一个requestPDU。RequestPDU会携带限制responsePDU返回数据大小的字段,比如
MaximumAttributeByteCount
:
BluetoothSDPProtocolPDU:ServiceSearchAttributeRequest(0x06)TransactionId:0xParameterLength:15ServiceSearchPattern:PublicBrowseGroupMaximumAttributeByteCount:AttributeIDListContinuationState:no(00)
ResponsePDU也会携带相应的字段表示自己返回数据的长度,比如
AttributeListsByteCount
:
BluetoothSDPProtocolPDU:ServiceSearchAttributeResponse(0x07)TransactionId:0xParameterLength:AttributeListByteCount:DataFragmentContinuationState:yes()ContinuationStateLength:2ContinuationStateValue
ContinuationState简介
上一节提到的
AttributeListsByteCount
不能比
MaximumAttributeByteCount
大。一旦server发现client请求的数据在一个responsePDU中放不下,就会把数据分段,并使用SDP定义的continuationstate完成所有分段的传输。每个数据段的大小不一定把
MaximumAttributeByteCount
用完,server可自行决定分段大小。比如在上面的responsePDU中分段大小为字节,远小于
MaximumAttributeByteCount
定义的最大值。
传输分段数据的responsePDU必须携带
ContinuationState
字段。该字段由两部分组成。其中
InfoLength
表示continuationinformation的长度,且最大值为0x10:
+------------+--------------------------+
InfoLength
ContinuationInformation
+------------+--------------------------+
Continuationinformation则是一个很神奇的字段,因为蓝牙核心规范并没有为它定义具体的含义,只要求该字段能解决分段传输的问题即可。具体的情形是,client收到携带分段数据的responsePDU后,会把其中的
ContinuationState
拿出来,放到下一个requestPDU中发回给server。然后server根据收到的
ContinuationState
确定下一个应传输的数据分段(这与cookie有异曲同工之妙),如此往复直到所有分段都传输完毕(传输最后一个分段的responsePDU不使能continuationstate)。
L2CAP导致的数据分段
在大多数实际场景中,并不是上一节阐述的原因导致了数据分段。比如当SDPserver返回的数据总和(bytes)远小于
MaximumAttributeByteCount
设置的时,也会有数据分段:
这是因为L2CAP(LogicalLinkControlandAdaptationProtocol)的限制。L2CAP承载了SDPPDU的传输,在L2CAP连接建立后,两端的设备会交换各自接收数据时支持的MTU(MaximumTransmissionUnit):
DeviceA-DeviceBBluetoothL2CAPProtocolLength:12CID:L2CAPSignalingChannel(0x)Command:ConfigureRequestCommandCode:ConfigureRequest(0x04)CommandIdentifier:0x04CommandLength:8DestinationCID:DynamicallyAllocatedChannel(0x).=Reserved:0x...............0=ContinuationFlag:FalseOption:MTUType:MaximumTransmissionUnit(0x01)Length:2MTU:DeviceB-DeviceABluetoothL2CAPProtocolLength:14CID:L2CAPSignalingChannel(0x)Command:ConfigureResponseCommandCode:ConfigureResponse(0x05)CommandIdentifier:0x04CommandLength:10SourceCID:DynamicallyAllocatedChannel(0x).=Reserved:0x...............0=ContinuationFlag:FalseResult:Success(0x)Option:MTUType:MaximumTransmissionUnit(0x01)Length:2MTU:
当SDPserver发现填入SDPresponsePDU的数据长度超过远端设备L2CAP设置的MTU时,就会使能continuationstate把数据分段传输。
Android定义的ContinuationInformation
前面说明了蓝牙核心规范并没有定义continuationinformation的具体含义是什么,于是Android对该字段的定义如下:
platform/system/bt/stack/sdp/sdpint.h#
cont_offset
的具体含义有两种:
当使用SDP_SERVICE_SEARCH_REQ/RSPPDU时,cont_offset表示下一个分段中起始数据项相对不分段完整数据项的偏移。这里的数据项具体指的是servicerecordhandle。比如server总共要返回个servicerecordhandle,第一次返回了10个,那么cont_offset就为10。也可以把这个偏移理解为当前已经传输的数据项总和或是后续传输的第一个数据项在完整数据项中的索引。当使用SDP_SERVICE_ATTR_REQ/RSP或SDP_SERVICE_SEARCH_ATTR_REQ/RSP时,cont_offset表示下一个分段的数据相对不分段完整数据的偏移。这和上面一种情况类似,只不过offset的单位由一条数据项变为了字节。当然也可以理解为已经传输的分段数据大小总和。比如下面传输第一个分段数据的responsePDU,其中cont_offset为,即,正好等于AttributeListsByteCount中的。
Client只要在后续的requestPDU中回传
cont_offset
,server就能找到对应responsePDU应携带分段数据的起始位置,从而继续传输分段数据。这满足了蓝牙核心规范解决分段传输问题的要求。
漏洞分析
Android实现的SDPserver在处理requestPDU时会进入
sdp_server_handle_client_req()
:
platform/system/bt/stack/sdp/sdp_server.cc#
该函数会根据PDUID判断当前requestPDU的类型。当requestPDU为
SDP_SERVICE_SEARCH_REQ
PDU时,进入
process_service_search()
做进一步处理:
platform/system/bt/stack/sdp/sdp_server.cc#
处理函数会依次提取
SDP_SERVICE_SEARCH_REQ
PDU携带的三个参数:
1、提取
ServiceSearchPattern
至
uid_seq
platform/system/bt/stack/sdp/sdp_server.cc#
2、提取
MaximumServiceRecordCount
至
max_replies
platform/system/bt/stack/sdp/sdp_server.cc#
3、提取
ContinuationState
至
cont_offset
platform/system/bt/stack/sdp/sdp_server.cc#
若当前处理的requestPDU使能了continuationstate,SDPserver就会提取其中的continuationinformation字段,并存入
cont_offset
。紧接着Android还会对
cont_offset
做安全检查,比较它与先前传给SDPclient的值是否相等,防止攻击者恶意设置offset导致数组越界:
platform/system/bt/stack/sdp/sdp_server.cc#
在得到
uid_seq
与
max_replies
后,SDPserver会找到所有与
uid_seq
匹配的servicerecordhandle,并把它们存储在
rsp_handles
数组中。这些handle就是client请求的数据。
max_replies
则用于限制这些handle的总数
num_rsp_handles
(漏洞点),避免返回的handle数量超过
MaximumServiceRecordCount
的限制:
platform/system/bt/stack/sdp/sdp_server.cc#
对于每一个请求,
num_rsp_handles
都会被重新计算一次。这种做法对不使用continuationstate的请求是合理的,因为若
uid_seq
和
max_replies
不同,
num_rsp_handles
也可能不同。但是当请求使用continuationstate时,所有上下文相同的请求都应使用同样的
uid_seq
和
max_replies
,那么
num_rsp_handles
不会改变,因此没必要再重新计算。如果重新计算,就应检查每次计算得到的
num_rsp_handles
是否相同。若不相同,则说明出现了异常流量,需要报错处理。但AOSP并没有做这种检查。
于是攻击者可以利用
max_replies
对
num_rsp_handles
的限制,使它的值在一个continuationstate上下文中发生变化。比如先发送普通的
SDP_SERVICE_SEARCH_REQ
PDU,正常触发continuationstate机制(为了增加触发continuationstate的概率,可以配置一个很小的L2CAPMTU),然后发送
MaximumServiceRecordCount
为1的continuationstatePDU,就会导致
num_rsp_handles
的值为1。
在
num_rsp_handles
值为1的情况下继续跟踪代码。当continuationstate使能时,SDPserver会使用
num_rsp_handles
减去
cont_offset
(已经传输的数量),从而得到剩余需要传输的handle数量
rem_handles
。由于
num_rsp_handles
被攻击者篡改为了1,且
rem_handles
的类型为
uint16_t
,所以
rem_handles
发生underflow:
platform/system/bt/stack/sdp/sdp_server.cc#
接下来SDPserver会根据远端设备L2CAP配置的MTU计算当前
SDP_SERVICE_SEARCH_RSP
PDU中最多能携带的handle数量
cur_handles
。显然下溢的
rem_handles
总是大于
cur_handles
,导致continuationstate恒使能:
platform/system/bt/stack/sdp/sdp_server.cc#
之后SDPserver会把位于
cont_offset
到
cont_offset+cur_handles
之间的handle写入
SDP_SERVICE_SEARCH_RSP
PDU。由于持续的continuationstate,会导致
cont_offset
不断增大,所以如下循环中的
rsp_handles[xx]
必然会发生越界读,最终导致内存泄露:
platform/system/bt/stack/sdp/sdp_server.cc#
References
ServiceDiscoveryProtocol(SDP)Specification,BLUETOOTHCORESPECIFICATIONVersion5.2
Vol3,PartBpageBlueBorneTechnicalWhitePaper