【漏洞分析】360 Vulpecker Team:BlueBorne 蓝牙漏洞深入分析与PoC
作者:huahuaisadog @ 360VulpeckerTeam
0x00
前些天,armis爆出了一系列蓝牙的漏洞,无接触无感知接管系统的能力有点可怕,而且基本上影响所有的蓝牙设备,危害不可估量,可以看这里(https://www.armis.com/blueborne/ )来了解一下它的逆天能力:只要手机开启了蓝牙,就可能被远程控制。现在手机这么多,利用这个漏洞写出蠕虫化的工具,那么可能又是一个手机版的低配wannacry了。我们360Vulpecker Team在了解到这些相关信息后,快速进行了跟进分析。 armis给出了他们的whitepaper,对蓝牙架构和这几个漏洞的分析可以说非常详尽了,先膜一发。不过他们没有给出这些漏洞的PoC或者是exp,只给了一个针对Android的“BlueBorne检测app",但是逆向这个发现仅仅是检测了系统的补丁日期。于是我来拾一波牙慧,把这几个漏洞再分析一下,然后把poc编写出来:
* CVE-2017-1000250 Linux bluetoothd进程信息泄露
* CVE-2017-1000251 Linux 内核栈溢出
* CVE-2017-0785 Android com.android.bluetooth进程信息泄露
* CVE-2017-0781 Android com.android.bluetooth进程堆溢出
* CVE-2017-0782 Android com.android.bluetooth进程堆溢出
以上PoC代码均在
https://github.com/marsyy/littl_tools/tree/master/bluetooth
由于也是因为这几个漏洞才从零开始搞蓝牙,所以应该有些分析不到位的地方,还请各路大牛斧正。
0x01 蓝牙架构及代码分布
这里首先应该祭出armis的paper里的图:
1
图上把蓝牙的各个层次关系描述得很详尽,不过我们这里暂时只需要关心这么几层:HCI,L2CAP,BNEP,SDP。BNEP和SDP是比较上层的服务,HCI在最底层,直接和蓝牙设备打交道。而承载在蓝牙服务和底层设备之间的桥梁,也就是L2CAP层了。每一层都有它协议规定的数据组织结构,所有层的数据包组合在一起,就是一个完整的蓝牙包(一个SDP包为例):
2
虽然协议规定的架构是图上说的那样,但是具体实现是有不同的,Linux用的BlueZ,而现在的Android用的BlueDroid,也就针对这两种架构说一说代码的具体分布。
BlueZ
在Linux里,用的是BlueZ架构,由bluetoothd来提供BNEP,SDP这些比较上层的服务,而L2CAP层则是放在内核里面。对于BlueZ我们对SDP和L2CAP挨个分析。
1, 实现SDP服务的代码在代码目录的/src/sdp,其中sdp-client.c是它的客户端,sdp-server.c是它的服务端。我们要分析的漏洞都是远程的漏洞,所以问题是出在服务端里面,我们重点关注服务端。而服务端最核心的代码,应该是它对接受到的数据包的处理的过程,这个过程由sdp-request.c来实现。当L2CAP层有SDP数据后,会触发sdp-server.c的io_session_event函数,来获取这个数据包,交由sdp-request.c的handle_request函数处理(怎么处理的,后续漏洞分析的时候再讲):
图3
2, L2CAP层的代码在内核里,这里我以Linux 4.2.8这份代码为例。l2cap层主要由 /net/bluetooth/l2capcore.c和/net/bluetooth/l2cap_sock.c来实现。l2cap_core.c实现了L2CAP协议的主要内容,l2cap_sock.c通过注册sock协议的方式提供了这一层针对userspace的接口。同样的我们关心一个L2CAP对接受到数据包后的处理过程,L2CAP的数据是由HCI层传过来的,在hci_core.c的hci_rx_work函数里
图4
收到数据后,会判断pkt_type,符合L2CAP层的type是HCI_ACLDATA_PKT,函数会走到hci_acldata_packet,这个函数会把HCI的数据剥离之后,把L2CAP数据交给L2CAP层的l2cap_recv_acldata:
图5
同样的,对于L2CAP层对数据的细致处理,我们还是等后续和漏洞来一块进行分析。
BlueDroid
在现在的Android里,用的是BlueDroid架构。这个和BlueZ架构有很大不同的一点是:BlueDroid将L2CAP层放在了userspace。SDP,BNEP,L2CAP统统都由com.android.bluetooth这个进程管理。而BlueDroid代码的核心目录在Android源码目录下的 /sytem/bt ,这个目录的核心产物是bluetooth.default.so,这个so集成所有Android蓝牙相关的服务,而且这个so没有导出任何相关接口函数,只导出了几个协议相关的全局变量供使用,所以想根据so来本地检测本机是否有BlueDrone漏洞,是一件比较困难的事情。对于BlueDroid,由于android的几个漏洞出在BNEP服务和SDP服务,所以也就主要就针对这两块。值得注意的是,在Android里,不论是64位还是32位的系统,这个bluetooth.default.so都是用的32位的。文章里这部分代码都基于Android7.1.2的源码。
1,BlueDroid的SDP服务的代码,在/system/bt/stack/sdp 文件夹里,其中sdp服务端对数据包的处理由sdp-server.c实现。SDP连接建立起来后,在收到SDP数据包之后呢,会触发回调函数sdp_data_ind,这个函数会把数据包交个sdp-server.c的sdp_server_handle_client_req函数进行处理:
图6
BlueDroid的BNEP服务的代码主要在/system/bt/stack/bnep/bnepmain.c。BNEP连接建立起来后,再收到BNEP的包,和SDP类似,会触发回调函数bnep_data_ind,这个函数包含了所有对BNEP请求的处理,漏洞也是发生在这里,具体的代码我们后续会分析。
0x02 漏洞分析以及PoC写法
蓝牙的预备知识差不多了,主要是找数据包的入口。我们再基于漏洞和PoC的编写过程来详细分析其中的处理过程,和相关蓝牙操作的代码该怎么写。
CVE-2017-1000251
这个是Linux L2CAP层的漏洞,那么就是内核里面的。先不着急看漏洞,先看L2CAP层如何工作。在一个L2CAP连接的过程中,我们抓取了它的数据包来分析,L2CAP是怎么建立起连接的:
图7
我们注意这么几个包: sent_infomation_request , send_connection_request, send_configure_request。抓包可以看到,在一次完整的L2CAP连接的建立过程中,发起连接的机器,会主动送出这么几个包。其中infomation_request是为了得到对方机器的名称等信息,connection_request是为了建立L2CAP真正的连接,主要是为了确定双方的CHANNEL ID,后续的数据包传输都要跟着这个channel id 走(图上的SCID, DCID),这个channel也就是我们所说的连接。在connection_request处理完毕之后,连接状态将变成 BT_CONNECT2 。随后机器会发起configure_request,这一步就到了armis的paper第十页所说的configuration process:
图8
这个过程完成后,整个L2CAP层的连接也就建立完成。
从上述过程看,可以发现L2CAP层连接的建立,主要是对上述三个请求的发起和处理。而我们的漏洞,也其实就发生在configuration process。我们先分析接收端收到这三个请求后,处理的逻辑在哪里,也就是我们前文提到的L2CAP对接受到的数据的处理过程:
1,在l2cap_recv_acldata接收到数据后,数据包会传给l2cap_recv_frame
2,l2cap_recv_frame会取出检查L2CAP的头部数据,然后检查根据头部里的cid字段,来选择处理逻辑:
图 9
3,底层L2CAP的连接,cid固定是L2CAP_CID_SIGNALING,于是会走l2cap_sig_channel,l2cap_sig_channel得到的是剥离了头部的L2CAP的数据,这一部将把数据里的cmd头部解析并剥离,再传给l2cap_bredr_sig_cmd进行处理:
图10
到这里,我们应该能得出L2CAP协议的数据结构:
图11
4, 随后数据进入到了l2cap_bredr_sig_cmd函数进行处理。这里也就是处理L2CAP各种请求的核心函数了:
图12
图13
好了,接下来终于可以分析漏洞了。我们的漏洞发生在对L2CAP_CONFIG_RSP(config response)这个cmd的处理上。其实漏洞分析armis的paper已经写的很详尽了,我这里也就权当翻译了吧,然后再加点自己的理解。那么来看l2cap_config_rsp:
14
当收到的数据包里,满足result == L2CAP_CONF_PENDING,且自身的连接状态conf_state == CONF_LOC_CONF_PEND的时候,会走到 l2cap_parse_conf_rsp函数里,而且传过去的buf是个长度为64的数据,参数len ,参数rsp->data都是由包中的内容来任意确定。那么在l2cap_parse_conf_rsp函数里:
图15
图16
仔细阅读这个函数的代码可以知道,这个函数的功能就是根据传进来的包,来构造将要发出去的包。而数据的出口就是传进去的64字节大小的buf。但是对传入的包的数据的长度并没有做检验,那么当len很大时,就会一直往出口buf里写数据,比如有64个L2CAP_CONF_MTU类型的opt,那么就会往buf里写上64*(L2CAP_CONF_OPT_SIZE + 2)个字节,那么显然这里就发生了溢出。由于buf是栈上定义的数据结构,那么这里就是一个栈溢出。 不过值得注意的是,代码要走进去,需要conf_state == CONF_LOC_CONF_PEND,这个状态是在处理L2CAP_CONF_REQ数据包的时候设置的:
图17
图18
当收到L2CAP_CONF_REQ的包中包含有L2CAP_CONF_EFS类型的数据【1】,而且L2CAP_CONF_EFS数据的stype == L2CAP_SERV_NOTRAFIC【2】的时候,conf_state会被置CONF_LOC_CONF_PEND
到这里,这个漏洞触发的思路也就清楚了:
1,建立和目标机器的L2CAP 连接,这里注意sock_type的选择要是SOCK_RAW,如果不是,内核会自动帮我们完成sent_infomation_request, send_connection_request, send_configure_request这些操作,也就无法触发目标机器的漏洞了。
2,建立SOCK_RAW连接,connect的时候,会自动完成sent_infomation_request的操作,不过这个不影响。
3,接下来我们需要完成send_connection_request操作,来确定SCID,DCID。完成这个操作的过程是发送合法的 L2CAP_CONN_REQ数据包。
4,接下来需要发送包含有L2CAP_CONF_EFS类型的数据,而且L2CAP_CONF_EFS数据的stype ==L2CAP_SERV_NOTRAFIC的L2CAP_CONF_REQ包,这一步是为了让目标机器的conf_state变成CONF_LOC_CONF_PEND。
5,这里就到了发送cmd_len很长的L2CAP_CONN_RSP包了。这个包的result字段需要是L2CAP_CONF_PENDING。那么这个包发过去之后,目标机器就内核栈溢出了,要么重启了,要么死机了。
这个漏洞是这几个漏洞里,触发最难的。
CVE-2017-1000250
这个漏洞是BlueZ的SDP服务里的信息泄露漏洞。这个不像L2CAP层的连接那么复杂,主要就是上层服务,收到数据就进行处理。那么我们也只需要关注处理的函数。 之前说过,BlueZ的SDP收到数据是从io_session_event开始。之后,数据的流向是:
1 |
iosessionevent–>handlerequest–>processrequest |
图19
有必要介绍一下SDP协议的数据结构: 它有一个sdp_pud_hdr的头部,头部数据里定义了PUD命令的类型,tid,以及pdu parameter的长度,然后就是具体的parameter。最后一个字段是continuation state,当一个包发不完所要发送的数据的时候,这个字段就会有效。对与这个字段,BlueZ给了它一个定义:
图20
对于远程的连接,PDU命令类型只能是这三个:SDP_SVC_SEARCH_REQ, SDP_SVC_ATTR_REQ, SDP_SVC_SEARCH_ATTR_REQ。这个漏洞呢,出现在对SDP_SVC_SEARCH_ATTR_REQ命令的处理函数里面 service_search_attr_req 。这个函数有点长,就直接说它干了啥,不贴代码了:
1, extract_des(pdata, data_left, &pattern, &dtd, SDP_TYPE_UUID); 解析service search pattern(对应SDP协议数据结构图)
2,max = getbe16(pdata); 获得Maximu Attribute Byte
3,scanned = extract_des(pdata, data_left, &seq, &dtd, SDP_TYPE_ATTRID);解析Attribute ID list
4,if (sdp_cstate_get(pdata, data_left, &cstate) < 0) ;获取continuation state状态cstate,如果不为0,则将包里的continuation state数据复制给cstate.
漏洞发生在对cstate状态不为0的时候的处理,我们重点看这部分的代码:
图21
sdp_get_cached_rsp函数其实是对cstate的timestamp值的检验,如何过这个检验之后再说。当代码走到【1】处的memcpy时,由于cstate->maxBytesSent就是由数据包里的数据所控制,而且没有做任何检验,所以这里可以为任意的uint16t值。那么很明显,这里就出现了一个对pResponse的越界读的操作。而越界读的数据还会通过SDP RESPONSE发送给攻击方,那么一个信息泄露就发生了。
写这个poc需要注意sdp_get_cached_rsp的检验的绕过,那么首先需要得到一个timestamp。当一次发送的包不足以发送完所有的数据的时候,会设置cstate状态,所以如果我们发给服务端的包里,max字段非常小,那么服务端就会给我们回应一个带cstate状态的包,这里面会有timestamp:
图22
所以,我们的poc应该是这个步骤:
1,建立SDP连接。这里我们的socket需要是SOCK_STREAM类型,而且connet的时候,addr的psm字段要是0x0001。关于连接的PSM:
图23
2,发送一个不带cstate状态的数据包,而且指定Maximu Attribute Byte的值非常小。这一步是为了让服务端给我们返回一个带timestamp的包。
3,接收这个带timestamp的包,并将timestamp提取。
4,发送一个带cstate状态的数据包,cstate的timestamp是指定为提取出来的值,服务端memcpy的时候,则就会把pResponse+maxBytesSent的内容发送给我们,读取这个数据包,则就获取了泄露的数据。
CVE-2017-0785
这个漏洞也是SDP的信息泄露漏洞,不过是BlueDroid的。与BlueZ的那个是有些类似的。我们也从对SDP数据包的处理函数说起。 SDP数据包会通过sdp_data_ind函数送给sdp_server_handle_client_req。与BlueZ一样,这个函数也会根据包中的pud_id来确定具体的处理函数。这个漏洞发生在对SDP_PDU_SERVICE_SEARCH_REQ命令的处理,对包内数据的解析与上文BlueZ中的大同小异,不过注意在BlueDroid中,cstate结构与BlueZ中有些不同:
图24
这里主要看漏洞:
图25
图26
①,②中代码可以看出,变量num_rsp_handles的值,一定程度上可以由包中的Maximu Attribute Byte字段控制。 ③中代码是对带cstate的包的处理,第一步是对大小的检查,第二步是获得cont_offset,然后对cont_offset进行检查,第三步就到了 rem_handles = num_rsp_handles – cont_offset 可以思考一种情况,如果num_rsp_handles < cont_offset,那么这个代码就会发生整数的下溢,而num_rsp_handles在一定程度上我们可以控制,而且是可以控制它变成0,那么只要cont_offset不为0,这里就会发生整数下溢。发生下溢的结果给了rem_handles,而这个变量代表的是还需要发送的数据数。 在④中,如果rem_handles是发生了下溢的结果,由于它是uint16_t类型,那么它将变成一个很大的数,所以会走到pccb->cont_offset += cur_handles;,cur_handles是一个固定的值,那么如果这个下溢的过程,发生很多次,pccb->cont_offset就会变得很大,那么在5处,就会有一个对rsp_handles数组的越界读的产生。
下面的操作可以让这个越界读发生:
1,发送一个不带cstate的包, 而且Maximu Attribute Byte字段设置的比较大。那么结果就是rem_handles = num_rsp_handles,而由于max_replies比较大,所以num_rsp_handles会成为一个比较大的值。只要在④中保证rem_handles> cur_handles,那么pccb->cont_offset就会成为一个非0值cur_handles。这一步是为了使得pccb->cont_offset成为一个非0值。
2,接收服务端的回应包,这个回应包里的cstate字段将会含有刚刚的pccb->cont_offset值,我们取得这个值。
3,发送一个带cstate的包,cont_offset指定为刚刚提取的值,而且设置Maximu Attribute Byte字段为0。那么服务端收到这个包后,就会走到rem_handles = num_rsp_handles – cont_offset从而发生整数下溢,同时pccb->cont_offset又递增一个cur_handles大小。
4,重复2和3的过程,那么pccb->cont_offset将越来越大,从而在⑤出发生越界读,我们提取服务端返回的数据,就可以获得泄露的信息的内容。
CVE-2017-0781
现在我们到了BNEP服务。BNEP的协议格式,下面两张图可以说明的很清楚:
图27
图28
BlueDroid中BNEP服务对于接受到的数据包的处理也不复杂:
1,解析得到BNEP_TYPE,得到extension位。
2,检查连接状态,如果已经连接则后续可以处理非BNEP_FRAME_CONTROL的包,如果没有建立连接,则后续只处理BNEP_FRAME_CONTROL的包。
3,去BNEP_TYPE对应的处理函数进行处理。
4,对于BNEP_TYPE不是BNEP_FRAME_CONTROL而且有extension位的,还需要对extension的数据进行处理。
5,调用pan层的回调函数。
值得注意的是,BNEP连接真正建立起来,需要先处理一个合法的BNEP_FRAME_CONTROL数据包。 CVE-2017-0781正是连接还没建立起来,在处理BNEP_FRAME_CONTROL时所发生的问题:
图29
上述代码中,malloc了一个remlen的大小,这个是和收到的数据包的长度相关的。可是memcpy的时候,却是从pbcb->p_pending_data+1开始拷贝数据,那么这里会直接溢出一个sizeof(*(pbcb->p_pending_data))大小的内容。这个大小是8.所以只要代码走到这,就会有一个8字节大小的堆溢出。而要走到这,只需要过那个if的判断条件,而这个if其实是对BNEP_SETUP_CONNECTION_REQUEST_MSG命令处理失败后的错误处理函数。那么只要发送一个错误的BNEP_SETUP_CONNECTION_REQUEST_MSG命令包,就可以进入到这段代码了触发堆溢出了。
所以我们得到poc的编写过程:
1,建立BNEP连接,这个和SDP类似,只是需要指定PSM为BNEP对应的0x000F。
2,发送一个BNEPTYPE为BNEP_FRAME_CONTROL,extension字段为1,ctrl_type为BNEP_SETUP_CONNECTION_REQUEST_MSG的错误的BNEP包:
图30
CVE-2017-0782
这个也是由于BNEP协议引起的漏洞,首先它是个整数溢出,整数溢出导致的后果是堆溢出。 问题出在BNEP对extension字段的处理上:
图31
图32
上述代码中,【1】的ext_len从数据包中获得,没有长度的检查,可为任意值。而当control_type为一个非法值的时候,会走到【2】,那么这里就很有说法了,我们如果设置ext_len比较大,那么这里就会发生一个整数下溢。从而使得rem_len变成一个很大的uint16_t的值。这个值将会影响后续的处理:
图33
上面的代码中,【1】处将发生整数下溢出,使得rem_len成为一个很大的值(比如0xfffd),【2】处会将这个值赋值给p_buf->len。【3】处是回调函数处理这个p_buf,在BlueDroid中这个函数是pan_data_buf_ind_cb,这个函数会有一条路径调到bta_pan_data_buf_ind_cback,而在这个函数中:
图34
memcpy用到了我们传进来的pbuf,而pbuf->len是刚刚下溢之后的很大的值,所以主要保证tBTA_PAN_DATA_PARAMS> pbuf->offset,这里就会发生一次很大字节的堆溢出。
代码首先要走到extension的处理,这个的前提是连接状态是BNEP_STATE_CONNECTED。而这个状态的建立,需要服务端先接收一个正确的BNEP_SETUP_CONNECTION_REQUEST_MSG请求包,同时要想pan_data_buf_ind_cb调用到bta_pan_data_buf_ind_cback产生堆溢出,需要在建立连接的时候指定UUID为UUID_SERV_CLASS_PANU可以阅读这两个函数来找到这样做的原因,这里就不再贴代码了。清楚这一点之后,我们就可以构造我们的poc了:
1,建立BNEP连接,这里只是建立起初步的连接,conn_state还不是BNEP_STATE_CONNECTED,这一步通过connect实现
2,发送一个正确的BNEP_SETUP_CONNECTION_REQUEST_MSG请求包,同时指定UUID为UUID_SERV_CLASS_PANU。这个包将是这样子: