BleedingTooth


I. 블루투스 취약점의 한계와 블리딩투스의 의의


해당 칼럼에서는 블루투스 호스트 공격인 Bleedingtooth에 대해 설명할 예정이다. Bleedingtooth는 제로클릭 원격 코드 실행 공격이다. 보통 블루투스 공격은 펌웨어를 타겟으로 하거나 정보를 도청하고 조작하는 수준으로 이루어지지만, Bleedingtooth는 타겟 디바이스를 완전히 제어할 수 있다는 점에서 차별점을 가진다.

II. 블루투스 동작 방식


Bleedingtooth의 자세한 attack surface는 BlueZ이다. BlueZ란 리눅스 상에서 블루투스 기능을 구현하기 위한 라이브러리이다. 공격자가 타겟의 BD 주소(Bluetooth Device address, 블루투스 장치마다 갖고 있는 고유 장치 주소)를 알고 있을 때, 조작된 l2cap 패킷을 보내서 RCE를 실현할 수 있다. 이후에 나올 취약점과 익스플로잇 설명을 위해 해당 단락에서 l2cap 프로토콜에 대해 짚고 넘어가겠다.

그림 1. 블리딩투스 공격 흐름의 도식

그림 1. 블리딩투스 공격 흐름의 도식

아래에서는 프로토콜의 이해뿐 아니라 익스플로잇에 대한 상세한 분석이 뒤따를 예정이므로 공격방법의 개요를 먼저 설명하겠다. 위의 도식을 보면 target이 malicous 패킷을 처리하는 과정에서 취약점이 발생하여 RCE 공격이 성사하는 것을 알 수 있다.
이때 L2CAP는 블루투스 프로토콜 스택의 한 계층이다. 블루투스는 host(PC)와 controller(bluetooth 칩, 드라이버 등) 사이에 HCI(host controller
interface)를 두고 데이터를 전송한다. 이때 HCI가 각 계층의 데이터를 원활하게 전송하기 위해 이용하는 것이 L2CAP라는 프로토콜이다. L2CAP는 채널 기반으로 작동하며, bleedingtooth에서 L2CAP가 사용하는 채널은 AMP이다(아래서는 a2mp로 언급된다).

III.BadChoice


Bleedingtooth가 공격하는 취약점으로는 BadChoice, BadKarma가 있다. 먼저 BadChoice는 CVE-2020-12352로, memory leak이 가능한 취약점이다. A2MP_GETINFO_REQ 함수를 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. static int a2mp_getinfo_req(struct amp_mgr *mgr, struct sk_buff *skb,
2. struct a2mp_cmd *hdr)
3. {
4. struct a2mp_info_req *req = (void *) skb->data;
5. ...
6. hdev = hci_dev_get(req->id);
7. if (!hdev || hdev->dev_type != HCI_AMP) {
8. struct a2mp_info_rsp rsp;
9.
10. rsp.id = req->id; 11. rsp.status = A2MP_STATUS_INVALID_CTRL_ID;
12.
13. a2mp_send(mgr, A2MP_GETINFO_RSP, hdr->ident, sizeof(rsp), &rsp);
14.
15. goto done;
16. }
17. ...
18. }

if문의 조건을 충족할 시, a2mp_info_rsp 타입 변수를 선언하고 초기화 후 send한다. 다만 a2mp_info_rsp 구조체엔 id, status뿐 아니라 더 많은 멤버들이 존재하며, 이 나머지 멤버들이 완전히 초기화되지 않았기 때문에 메모리 leak이 가능할 수 있다.

IV.BadKarma


BadKarma로 불리는 CVE-2020-12351은 Type confusion 취약점이다. Type confusion 취약점은 주로 데이터 타입 불일치에서 발생하는 취약점이다.
1
2
3
4
5
6
1. static int l2cap_data_rcv(struct l2cap_chan *chan, struct sk_buff *skb) {
2. ...
3. if ((chan->mode == L2CAP_MODE_ERTM || chan->mode == L2CAP_MODE_STREAMING) && sk_filter(chan->data, skb))
4. goto drop;
5. ...
6. }

l2cap_data_rcv에서 streaming 혹은 ERTM(Enhanced Retransmission Mode) 모드가 사용될 때 sk_filter가 호출된다. 이때 호출하는 인자는 chan-> data이다.

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
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static struct l2cap_chan *a2mp_chan_open(struct l2cap_conn *conn, bool locked)
{
struct l2cap_chan *chan;
int err;

chan = l2cap_chan_create();
if (!chan)
return NULL;
...
chan->mode = L2CAP_MODE_ERTM;
...
return chan;
}
...
static struct amp_mgr *amp_mgr_create(struct l2cap_conn *conn, bool locked)
{
struct amp_mgr *mgr;
struct l2cap_chan *chan;

mgr = kzalloc(sizeof(*mgr), GFP_KERNEL);
if (!mgr)
return NULL;
...
chan = a2mp_chan_open(conn, locked);
if (!chan) {
kfree(mgr);
return NULL;
}

mgr->a2mp_chan = chan;
chan->data = mgr;
...
return mgr;
}

위의 amp_mgr_create()를 보면 이 chan->data의 타입이 struct amp_mgr임을 알 수 있다. 반면 sk_filter는 struct sock 타입의 데이터를 인자로 취한다. 이러한 type confusion 취약점은 리눅스 커널 4.8에서 발생했다.


V.BleedingTooth exploit

앞서 설명한 BadChoice와 BadKarma를 이용하여 RCE 공격을 진행할 수 있다. 아래 순차적으로 정리한 공격 과정을 살펴보도록 하자.

I.BadKarma 우회

아이러니하게도, BadKarma 취약점을 exploit하기 위해서는 바로 그 BadKarma를 우회할 필요가 있다. 이유는 다음과 같다. 공격 도식에서 보았듯 Bleedingtooth 공격을 위해서는 A2MP 채널을 생성하고 초기화해야 한다. 이 A2MP 채널은 ERTM/streaming 모드로 구성되어 있는데, 해당 커널 코드에서 ERTM/streaming 모드는 l2cap_data_rcv() 함수의 sk_filter를 거쳐야 한다. 그리고 이 경로는 sk_filter의 코드적 결함(type confusion) 탓에 패닉을 일으켜 A2MP 프로토콜 처리까지 도달할 수 없게 만든다. 따라서 공격을 성공시키기 위해 BadKarma를 먼저 우회해야 한다.
L2CAP의 코드를 살펴 보면 ERTM, Streaming 외에 UNACCEPT 모드가 존재한다. (모드 변경은 패킷 내의 특정 if문 조작으로 이루어진다.) 해당 모드 패킷을 보내면 l2cap_data_rcv 대신 서브루틴 l2cap_parse_conf_rsp를 호출한다. 이렇게 sk_filter로 인한 패닉을 우회해 A2MP 명령을 처리한다.

II. sk_filter() 조사

앞서 살펴보았듯, BadKarma의 취약점은 sock 타입 인자를 받는 sk_killer에 amp_mgr 인자를 패스함으로써 발생했다. 이는 달리 말하자면 sock의 필드가 amp_mgr의 필드에 잘못 매핑되는 것을 역이용하여 포인터 역참조를 할 수 있다면 구조체 amp_mgr의 다른 멤버를 제어할 수도 있다는 뜻이다. 그리고 amp_mgr의 멤버들을 제어할 수 있다면 당연히 이를 인자로 받는 sk_filter()의 코드 플로우 조작도 가능해진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// pahole -E -C sock --hex bluetooth.ko
struct sock {
struct sock_common {
...
short unsigned int skc_family; /* 0x10 0x2 */
...
} __sk_common; /* 0 0x88 */
...
struct sk_filter * sk_filter; /* 0x110 0x8 */
...
/* size: 760, cachelines: 12, members: 88 */
/* sum members: 747, holes: 4, sum holes: 8 */
/* sum bitfield members: 40 bits (5 bytes) */
/* paddings: 1, sum paddings: 4 */
/* forced alignments: 1 */
/* last cacheline: 56 bytes */
} __attribute__((__aligned__(8)));

그리고 여기서는 struct sock의 sk->sk_filter만이 유일하게 트리거에 활용 가능하다. (이에 대한 상세한 분석은 초심자용 글이니만큼 생략하도록 하겠다. 이유가 궁금한 독자는 ‘참고자료’ 파트의 원문 참고 바람.) 이 sk_filter는 sock 구조체에서 offset 0x110에 위치한다. Amp_mgr의 구조체 크기는 0x70바이트이기 때문에 단순히 amp_mgr만을 조작하여 sk_filter를 손상시키기는 어렵다. 다만, heap을 마음대로 구성할 수 있다면 sk_filter를 건드릴 수 있다. 커널이 직접 접근하는 메모리 영역은 slab이다.

heap primitive 찾기

heap을 구성하기 위해서는 동적 할당을 하는 primitive를 찾아야 한다. Primitive는 힙의 할당을 돕는 코드 조각을 의미하며, 조건을 아래와 같다;

  • 일정 크기의 메모리를 할당할 수 있다.
  • 공격자가 제어하는 데이터를 복사할 수 있다.
  • 해제할 때까지 메모리를 남겨둘 수 있어야 한다.

위 a, b를 충족하는 조건이 kmemdup 함수이며, 이 함수는 A2MP_GETAMPASSOC_RSP 명령에 존재한다. c조건의 경우 이를 충족하는 적절한 함수가 없어서 HCI 연결을 닫는 방법으로 진행했다(이 경우 다소 원시적이고 시간이 오래 걸린다는 점을 감안해야 한다).

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
1. static int a2mp_getampassoc_rsp(struct amp_mgr *mgr, struct sk_buff *skb, struct a2mp_cmd *hdr)
2. {
3. ...
4. u16 len = le16_to_cpu(hdr->len);
5. ...
6. assoc_len = len - sizeof(*rsp);
7. ...
8. ctrl = amp_ctrl_lookup(mgr, rsp->id);
9. if (ctrl) {
10. u8 *assoc;
11.
12. assoc = kmemdup(rsp->amp_assoc, assoc_len, GFP_KERNEL);
13. if (!assoc) {
14. amp_ctrl_put(ctrl);
15. return -ENOMEM;
16. }
17.
18. ctrl->assoc = assoc;
19. ctrl->assoc_len = assoc_len;
20. ctrl->assoc_rem_len = assoc_len;
21. ctrl->assoc_len_so_far = 0;
22.
23. amp_ctrl_put(ctrl);
24. }
25. ...
26. }

이 primitive를 이용하여 kmalloc-128 slab을 형성해줄 것이다. assoc = kmemdup(rsp->amp_assoc, assoc_len, GFP_KERNEL);을 살펴보면, kmemdup에서 복사가 가능하다. 위 함수로 kmemdup을 사용하기 위해서는 a2mp_getinfo_rsp를 이용해서 ctrl(변수)에 값을 넣어줘야 한다. 중요한 내용은 아니므로 이 정도만 설명하고 넘어가겠다.

IV. OOB control

위의 primitve를 이용하여 특정 공간에 같은 페이로드를 반복적으로 분사하는 spray 공격을 진행할 것이다. 128바이트의 오브젝트들을 많이 할당하여 kmalloc-128 슬랩을 채우고, 새 A2MP 채널을 생성한 뒤 amp_mgr 구조체가 스프레이된 객체와 인접하여 오프셋 0x110의 값 제어가 가능하게 한다. 이후에 type confusion으로 역참조에 성공하면 OOB가 가능해진다.

그림 2. OOB control

그림 2. OOB contorl

V. Memory layout leak

이제 sk_filter가 역참조하는 포인터 제어가 가능해졌다. 그럼 이제 이 포인터로 어디를 가리켜야 할까? 해당 부분에서 BadChoice 취약점을 사용하여 포인터를 leak할 것이다.
앞서 그러했듯이, 초기화되지 않은 스택 변수 버그를 악용하기 위해서는 먼저 몇 가지 명령을 전송하여 스택 프레임에 특정 데이터들을 채워두어야 한다. 이후 vulnerable command로 이 데이터들을 받아내면 공격이 성공한다. L2CAP_CONF_RSP를 전송하고 A2MP 채널을 ERTM 모드로 변경하려 함으로써, 위에 있던 A2MP 서브 루틴이 실행되면서 포인터가 유출되는데, 이때 l2cap_chan 객체 주소가 유출 될 수 있다.
l2cap_chan은 792바이트로, kmalloc-1024 slab에 할당된다. L2cap_chan의 주소를 유출하고 A2MP
채널을 해제하여 l2cap_chan도 해제한다. 다시 연결하여 kmalloc-1024 slab을 spray 한다. 이때 이전에 할당받은 l2cap_chan 객체 주소도 포함되어 있을 것이다. (확률적인 부분이 있으나 익스플로잇에 있어서 중요한 계산은 아니므로 생략. 마찬가지로 전 과정이 궁금한 독자는 참고자료의 원문 참고 바람.)

VI. RIP control

Sk_filter로 RIP를 제어할 수 있다. Sk_filter는 sk_filter_trim_cap 함수를 리턴한다. 여기서 사용하
는 중요한 멤버들을 나열하면 아래와 같다.

1
sk->sk_filter->prog->bpf_func(skb, sk->sk_filter->prog->insnsi);

마지막 bpf_func의 두번째 인수 또한 sk->sk_filter에서 출발하므로 제어가 가능하다. 호출규약에
따라 두번째 인수는 RSI 레지스터에 담긴다.

VII. Kernel Stack pivoting

NX 보호기법으로 인해 shellcode를 직접 실행할 수 없다. ROP를 사용하기 위해서는 KASLR 우회를 해야한다. 때문에 RSP를 ROP 가젯이 있는 페이로드의 가짜 스택으로 리다이렉션하는 방향으로 잡았다. 6번 단락에서 RSI 레지스터도 우리가 제어할 수 있는 인자 중 하나라고 설명하였다. 이 RSI 값을 RSP로 옮기면 스택 피벗팅이 가능하다. 이때 사용한 가젯은 ROPgadget 툴을 이용하여 /boot/vmlinuz에서 추출하였다. 여기까지의 모든 과정을 끝내면 RCE를 달성할 수 있다.

그림 3. Kernal Stack pivoting

그림 3. Kernal Stack pivoting

VI.닫는 글

블루투스가 실생활에서 쉽게 접할 수 있는 통신 기술이기 때문에 주제로 선정했다. CVE 자체는 2020년이라 오래된 감이 있지만, 블루투스 통신 기술부터 heap 관련 익스플로잇 기술까지 폭넓게 공부할 수 있었다.
칼럼을 쓰면서 프로토콜과 커널 시스템 간의 유기성을 잘 연결하여 이해하기 쉽게 작성하려고 노력하였다. 해당 칼럼 독자를 커널 입문자로 생각하고 작성하다보니 slab 메모리 관리 등 cs 개념을 좀 더 자세히 싣고, 원글에서의 MTU 제한 등 까다로운 조건 해결 등은 과감히 넘어갔다. 디테일한 부분을 더 살펴보고 싶다면 참고문헌의 Bleedingtooth 원글을 찾아보는 것을 추천한다.

Ⅶ. 참고문헌

Bleedingtooth. (n.d.). Google Security-Research. https://github.com/google/security-research/tree/master/pocs/linux/bleedingtooth