[Linux] BleedingTooth: Linux Bluetooth Zero-Clink RCE
개요
해당 취약점은 Linux Kernel
기능 중 Bluetooth
에서 발생하는 아래의 3개 취약점을 연계해서 RCE까지 발생하는 취약점 입니다.
- Heap-Based Buffer Overflow(BadVibes): CVE-2020-24490
- Stack-Based Information Leak(BadChoice): CVE-2020-12352
- Heap-Based Type Confusion(BadKarma): CVE-2020-12351
해당 취약점들에 대해서 하나씩 RCA를 설명드리겠습니다.
💡 Target: Linux Kernel 4.19
해당 버전을 기준으로 포스터를 작성했습니다.
Heap-Based Buffer Overflow(BadVibes): CVE-2020-24490
CVE-2020-24490
의 패치 내역은 링크에 있습니다. 해당 링크를 보면 net/bluetooth/hci_event.c
파일에 존재하는 함수들이 변경된 것을 확인할 수 있습니다. 즉, 해당 함수들에서 취약점이 발생했다는 것을 의미합니다. 패치 전 기준으로 해당 함수들을 살펴보겠습니다.
hci_le_adv_report_evt
hci_le_adv_report_evt
함수는 process_adv_report
함수를 호출하게 되는데 ev→data
, ev→length
데이터가 넘어가게됩니다. 여기서, hci_le_adv_report_evt
함수에서는 길이 검증이 존재하지만 hci_le_ext_adv_report_evt
함수는 길이 검증이 존재하지 않아서 취약점이 발생하게 됩니다.
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
static void hci_le_adv_report_evt(struct hci_dev *hdev, struct sk_buff *skb)
{
u8 num_reports = skb->data[0];
void *ptr = &skb->data[1];
hci_dev_lock(hdev);
while (num_reports--) {
struct hci_ev_le_advertising_info *ev = ptr;
s8 rssi;
if (ev->length <= HCI_MAX_AD_LENGTH) {
rssi = ev->data[ev->length];
process_adv_report(hdev, ev->evt_type, &ev->bdaddr,
ev->bdaddr_type, NULL, 0, rssi,
ev->data, ev->length);
} else {
bt_dev_err(hdev, "Dropping invalid advertising data");
}
ptr += sizeof(*ev) + ev->length + 1;
}
hci_dev_unlock(hdev);
}
...
static void hci_le_ext_adv_report_evt(struct hci_dev *hdev, struct sk_buff *skb)
{
u8 num_reports = skb->data[0];
void *ptr = &skb->data[1];
hci_dev_lock(hdev);
while (num_reports--) {
struct hci_ev_le_ext_adv_report *ev = ptr;
u8 legacy_evt_type;
u16 evt_type;
evt_type = __le16_to_cpu(ev->evt_type);
legacy_evt_type = ext_evt_type_to_legacy(evt_type);
if (legacy_evt_type != LE_ADV_INVALID) {
process_adv_report(hdev, legacy_evt_type, &ev->bdaddr,
ev->bdaddr_type, NULL, 0, ev->rssi,
ev->data, ev->length);
}
ptr += sizeof(*ev) + ev->length + 1;
}
hci_dev_unlock(hdev);
}
process_adv_report
process_adv_report
함수도 마찬가지로 store_pending_adv_report
함수에 data
변수와 len
변수에 전달하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void process_adv_report(struct hci_dev *hdev, u8 type, bdaddr_t *bdaddr,
u8 bdaddr_type, bdaddr_t *direct_addr,
u8 direct_addr_type, s8 rssi, u8 *data, u8 len)
{
...
if (!has_pending_adv_report(hdev)) {
/* If the report will trigger a SCAN_REQ store it for
* later merging.
*/
if (type == LE_ADV_IND || type == LE_ADV_SCAN_IND) {
store_pending_adv_report(hdev, bdaddr, bdaddr_type,
rssi, flags, data, len);
return;
}
mgmt_device_found(hdev, bdaddr, LE_LINK, bdaddr_type, NULL,
rssi, flags, data, len, NULL, 0);
return;
}
...
}
store_pending_adv_report
store_pending_adv_report
해당 함수에서 memcpy를 통해 복사하게 되지만, hci_le_ext_adv_report_evt
함수를 거쳐서 길이 검증이 없는 경우에 BOF가 발생하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
static void store_pending_adv_report(struct hci_dev *hdev, bdaddr_t *bdaddr,
u8 bdaddr_type, s8 rssi, u32 flags,
u8 *data, u8 len)
{
struct discovery_state *d = &hdev->discovery;
bacpy(&d->last_adv_addr, bdaddr);
d->last_adv_addr_type = bdaddr_type;
d->last_adv_rssi = rssi;
d->last_adv_flags = flags;
memcpy(d->last_adv_data, data, len);
d->last_adv_data_len = len;
}
여기서 hci_dev
구조체의 discovery
멤버부터 overwrite되기 때문에 이를 이용해서 구조체의 멤버를 조작할 수 있습니다.
해당 구조체는 다음과 같이 정의 되어있는데, discovery
멤버 이후 값들 중 함수 포인터를 변조해 원하는 RIP 조작이 가능하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct hci_dev {
...
struct discovery_state discovery;
...
int (*open)(struct hci_dev *hdev);
int (*close)(struct hci_dev *hdev);
int (*flush)(struct hci_dev *hdev);
int (*setup)(struct hci_dev *hdev);
int (*shutdown)(struct hci_dev *hdev);
int (*send)(struct hci_dev *hdev, struct sk_buff *skb);
void (*notify)(struct hci_dev *hdev, unsigned int evt);
void (*hw_error)(struct hci_dev *hdev, u8 code);
int (*post_init)(struct hci_dev *hdev);
int (*set_diag)(struct hci_dev *hdev, bool enable);
int (*set_bdaddr)(struct hci_dev *hdev, const bdaddr_t *bdaddr);
};
Stack-Based Information Leak(BadChoice): CVE-2020-12352
해당 취약점이 발생하는 함수 코드는 다음과 같습니다.
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
static int a2mp_getinfo_req(struct amp_mgr *mgr, struct sk_buff *skb,
struct a2mp_cmd *hdr)
{
struct a2mp_info_req *req = (void *) skb->data;
struct hci_dev *hdev;
struct hci_request hreq;
int err = 0;
if (le16_to_cpu(hdr->len) < sizeof(*req))
return -EINVAL;
BT_DBG("id %d", req->id);
hdev = hci_dev_get(req->id);
if (!hdev || hdev->dev_type != HCI_AMP) {
struct a2mp_info_rsp rsp;
rsp.id = req->id;
rsp.status = A2MP_STATUS_INVALID_CTRL_ID;
a2mp_send(mgr, A2MP_GETINFO_RSP, hdr->ident, sizeof(rsp),
&rsp);
goto done;
}
...
}
요청을 받아서 a2mp_getinfo_req
함수가 트리거되면 rsp
변수가 반환되게 됩니다. 여기서 rsp
변수는 a2mp_info_rsp
구조체이지만 해당 함수에서 id
와 status
만 초기화하고 있습니다.
1
2
3
4
5
6
7
8
9
struct a2mp_info_rsp {
__u8 id;
__u8 status;
__le32 total_bw;
__le32 max_bw;
__le32 min_latency;
__le16 pal_cap;
__le16 assoc_size;
} __packed;
해당 변수는 스택 영역에 생성되게 때문에 스택의 메모리가 leak
되는 취약점입니다.
Heap-Based Type Confusion(BadKarma): CVE-2020-12351
우선, 아래의 함수에서 만약 chan
변수가 없다면 생성한 뒤에 mode
가 L2CAP_MODE_STREAMING
이면 l2cap_data_rcv
함수를 호출하고 있습니다.
💡 만약
chan
변수가 없는 경우에는chan
변수의mode
멤버가L2CAP_MODE_ERTM
으로 선언되게 됩니다.
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
static void l2cap_data_channel(struct l2cap_conn *conn, u16 cid,
struct sk_buff *skb)
{
struct l2cap_chan *chan;
chan = l2cap_get_chan_by_scid(conn, cid);
if (!chan) {
if (cid == L2CAP_CID_A2MP) {
chan = a2mp_channel_create(conn, skb);
if (!chan) {
kfree_skb(skb);
return;
}
l2cap_chan_lock(chan);
} else {
BT_DBG("unknown cid 0x%4.4x", cid);
/* Drop packet and return */
kfree_skb(skb);
return;
}
}
...
switch (chan->mode) {
...
case L2CAP_MODE_ERTM:
case L2CAP_MODE_STREAMING:
l2cap_data_rcv(chan, skb);
goto done;
...
}
}
l2cap_data_rcv
함수는 다음과 같습니다. 다양한 조건문을 통과하게 되면 sk_filter
함수로 chan→data
인자와 skb
인자를 전달하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
static int l2cap_data_rcv(struct l2cap_chan *chan, struct sk_buff *skb)
{
struct l2cap_ctrl *control = &bt_cb(skb)->l2cap;
u16 len;
u8 event;
...
if ((chan->mode == L2CAP_MODE_ERTM ||
chan->mode == L2CAP_MODE_STREAMING) && sk_filter(chan->data, skb))
goto drop;
...
}
여기서 sk_filter
함수의 정의를 보면 다음과 같습니다. 하지만, 해당 함수가 호출될 때 chan
변수가 존재하지 않았다면 이전 로직에서 chan→data
에 sock
구조체가 아닌 값이 들어갈 수도 있기 때문에 Type Confusion
취약점이 발생하게 됩니다.
1
int sk_filter(struct sock *sk, struct sk_buff *skb);