Post

[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 구조체이지만 해당 함수에서 idstatus만 초기화하고 있습니다.

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 변수가 없다면 생성한 뒤에 modeL2CAP_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→datasock 구조체가 아닌 값이 들어갈 수도 있기 때문에 Type Confusion 취약점이 발생하게 됩니다.

1
int sk_filter(struct sock *sk, struct sk_buff *skb);

참고 사이트

This post is licensed under CC BY 4.0 by the author.