前言
最近看到新出的星闪 有点兴趣 所以打算先从基础的蓝牙学起 顺便借此机会熟悉一下通讯以及协议相关的知识 本文用来记录关于蓝牙的一些研究 不做教学用途
蓝牙是什么
蓝牙(Bluetooth)是一种无线通讯技术标准 用来让固定设备(手机 电脑等)以及移动设备(耳机 鼠标等)在短距离之间交换数据 形成个人局域网(PNA)
其使用短波特高频 在2.4G到2.485G的ISM频段进行通讯
蓝牙技术分为BR/EDR(基本速率/增强数据率)和LE(低耗能)两种
前者是传统的蓝牙传输模式 拥有较高的传输速率 适用于需要较高宽带的应用 并且采用的是点对点的网络拓扑 即1对1通信
后者的功耗需求更低 提供了更快的连接 但是传输速率较小 适用于低功耗应用和设备 其采用点对点(1对1) 广播(1对多) 网格(多对多)等网络拓扑结构
二者是相互独立的传输模式 都有自己的协议栈和通讯方式 但是二者并非不可共存 我们称双模式设备为蓝牙双模设备
目前蓝牙由蓝牙技术联盟来负责维护技术标准 其并不负责制造蓝牙设备
上图为蓝牙各代版本的发展 自4.0以后 分为了BR/EDR和LE两种模式
本文所要详细研究的BLE 并不等于蓝牙4.0 其只是4.0的一个部分
低功耗蓝牙协议栈
先明白协议栈是一个什么东西
他是指网络通信中所有协议的一个集合 由多个层级组成
协议栈通常由三个部分组成 媒体 传输 应用
来看看TCP/IP协议栈
其不仅包括TCP IP协议 还包括http ip dns tcp arp等协议
该协议有一个参考模型 所有的协议都归类到四个层级中 分别是链路层 网络层 传输层 应用层 每一层使用其下一层的协议来完成需求
具体的层级利用流程这里就不延申了 回到蓝牙协议栈 这里只介绍低功耗蓝牙BLE的协议栈
其也由三个部分组成 Host + HCI + Controller
主机(Host) 这部分由核心协议层(L2CAP、SM、ATT)和核心规范(GAP、GATT)构成
控制器(Controller) 分为Link Layer(链路层)和Physical Layer(物理层)
HCI 此部分定义了主机和控制器之间的通信标准 比如UART或者USB
接下来拆分各层进行概念的解析 更具体的深入随着后续的抓包分析进行
物理层
BLE的物理层定义了无线电接收器/发射器如何编码和解码传输的数据 以及应用的无线电的其他参数
其在2.4G到2.485G的免授权频段工作 采用40个信道 每个信道间隔2MHZ
分为广播信道和数据信道两个部分 广播频道只占用3个信道 固定为最后三个信道 用于发现设备 初始化连接以及广播数据
建立连接的设备 必须在同一信道 同一时间上才能传输数据
链路层
该部分参考《core_v5.3.pdf》中的Part B: Link Layer Specification章节 即2662页起
下载地址: https://www.bluetooth.com/zh-cn/specifications/specs/core-specification-5-3/
链路层起到的作用很多 比如负责指定选择哪个射频通道进行通信 怎么确保数据的完整性 具体在哪个时间点把包发送出去等等 其是整个BLE协议栈的核心 不过链路层只负责数据的发送和接收 对于数据的解析则交给GAP和GATT
射频状态
共有五种射频状态 LL层可以控制设备处于这五种状态中的一种
待机 通告 扫描 初始化 连接
待机状态(Standby State) 此时既不发送数据 也不接收数据 设备此时最节能
通告状态(Advertising State) 处于此状态的设备称为”通告者” 会周期性的通过广播通道发送数据 可以由处于初始化状态以及扫描状态的设备接收到
扫描状态(Scanning State) 处于该状态的设备成为”扫描者” 可以通过广播通道接收数据
初始化状态(Initiating State) 与扫描状态相似 不过其只会监听特定设备的广播 并且在接收数据后 还会发起连接请求
连接状态(Connection State) 由通告状态和初始化状态自动切换 处于该状态的设备由两种角色 主设备和从设备
数据包格式
在以往的学习过程中 复现过一个tddp报文的协议洞 同理 来看一下BLE的报文的基础格式
其包含4个字段 前导码 访问地址 协议数据单元(PDU) 循环冗余检验
在建立连接的过程中 使用广播通道(PDU) 传输数据用数据通道(PDU)
其中 广播报文和数据报文的格式也不一样
广播报头包括 广播报文类型(4字节) RFU(无实际作用 用来对齐字节 1字节) ChSel(无实际作用 用来对齐字节 1字节) TxAdd(发送地址类型 1字节) RxAdd(接收地址类型 1字节) Length(用来表示payload的长度 8字节)
数据报头包括 LLID(逻辑链路标识符 2字节) NESN(下一个预期序列号 1字节) SN(序列号 1字节) MD(更多数据 1字节) CP(报文类型 4字节)RFU(保留 3字节) length(payload的长度 8字节) CTEinfo (连续音扩展 8字节)
HCI层
其为主机和控制器之间提供一组标准接口
主要的3个任务:
-将主机的命令下达给控制器
-控制器将事件传送给主机
-在连接通道上进行数据传输
这里的接口不单单指物理接口 也包括逻辑接口 逻辑接口定义了命令 事件 数据的封包格式 物理接口定义了主机和控制器之间如何传输数据
L2CAP层
该层功能较多 其支持数据的分割和重组 通道复用等等
其将PDU(协议数据单元)拆分成更小的片段 这样就可以在不同信道中传输 提高数据吞吐量以及更高的并发性
他允许低功耗蓝牙(BLE)在同一时间复用三条信道
SM层
sm层的主要功能是负责ble连接建立 认证 解密等行为的安全性
ATT层
该层译文为数据交互协议
引入一个新概念 “属性”
可以认为一个属性就是一段数据 每一个属性都包含四个部分
1.句柄 用来标识属性的唯一
2.类型 用来存储属性的类型
3.值 用来存储属性的具体值
4.权限 用来规范执行操作的权限
GATT层
该层负责整合ATT层的属性 将其统筹为”服务”
一个BLE设备可以包含多个服务 一个服务可以包含多个特征 一个特征可以包含多个描述符
GATT用来规范特征中的内容
GAP层
该层定义了设备如何相互发现 连接 绑定以及描述了设备如何成为广播者和观察者
还定义了不同类型的地址来实现隐私性和可解析性
GAP有两个概念来定义设备的行为 模式和过程
模式指设备长时间进行的操作 例如广播模式 指设备正在进行广播 广播需要较长时间
过程指短时间内设备进行的操作 例如一个设备正在寻找广播 观察所需的时间较短
其一共定义了四个角色 广播者 观察者 外围设备 中央设备
这里区分是否外围的要素为 是否主动发起连接 主动的一方是中央设备
BLE设备工作流程
配对模式
大体的流程分为
蓝牙启动->扫描设备->设备配对->数据传输
其中 蓝牙的设备配对可以分为四种
1.Numeric Comparison
配对双方都显示一个6位数字 用户核验数字一致后 即可配对成功
2.Just Works
用户看不到配对过程 主动发起连接即可配对 比如蓝牙耳机 开盖即可
3.Passkey Entry
与第一种较类似 不过是在配对目标(比如电脑)上输入本地设备上显示的6位数字
例如wallpaper手机端和电脑端的壁纸传输
4.Out of Band
通过其他途径交换配对信息 例如NTF
配对认证
蓝牙2.0协议之前确保配对过程安全的是凭借PIN码 PIN码长度可以为4到16位数字
在配对过程中 会根据PIN码来生成一个Linkkey 两个配对设备共享一个Linkkey 我们称这个行为为绑定 下次配对时 即可用Linkkey来认证
后续的协议凭借密钥交换来配对 分为生成初始密钥 生成链路密钥 双方认证三个过程
生成初始密钥
首先引入几个概念 我们称提出通信要求的设备为主设备 被动进行通信的设备为从设备
初始密钥的长度为128bit 由k22算法生成 该算法需要三个参数
1.从设备的物理地址BR_ADDR 每一个蓝牙设备都会被分配一个唯一的48bit的BR_ADDR 主设备在生成密钥之前 通过询问的方式来获取从设备的蓝牙MAC地址
2.PIN码及其长度 PIN码是双方设备预先设定的
3.一个128bit的随机数 由主设备生成 以明文的方式发送给从设备
如果双方设备以上获取到三部分的值相同 因为采用的是相同的k22算法 所以生成的初始密钥(Kinit)也是一致的
生成链路密钥
首先主设备生成128位随机数LK_RANDA 从设备也生成128位随机数LK_RANDB
主设备中 LK_RANDA和Kinit进行位比特逻辑异或运算 将结果发送给从设备
从设备中 LK_RANDB和Kinit进行位比特逻辑异或运算 将结果发送给主设备
此时主从设备中都具有相同的Kinit LK_RANDA LK_RANDB BR_ADDRA BR_ADDRB
此时利用E21算法将这些进行加密 并将结果异或得到Kab
双方认证
双方认证采用challenge-response方式
主设备A为应答方 从设备B为请求方
主设备A生成128bit的随机数AU_RANDA 以明文的形式传输给从设备B
双方利用E1算法 将各自得到的Kab AU_RANDA BD_ADDRB加密运算分别得到32位的SRESA和SRESB
B设备将SRESB发送给A A设备对比SRESA和SRESB 如果一致即认证通过
接下来交换双方的角色 重新进行一次认证
烧录canokey固件
补:忙活了大半天 貌似这个固件不是用来搞蓝牙的…..被误导了 那就权当记录一下吧
我买的蓝牙适配器是亿佰特E104-BT5040U 这一款是需要自己额外烧录固件才能使用的
那没办法了 接下来再学一手烧录固件
查阅了一些资料后 我选择CanoKey来刷入 https://github.com/canokeys/canokey-nrf52
首先需要确保前置工具配置完全
Prerequisites: 先决条件: |
其他应该都安装过 这里提供GNU ARM的安装方式
sudo apt install gcc-arm-none-eabi |
那么接下来开始编译固件
git clone https://github.com/canokeys/canokey-nrf52.git |
执行cmake的时候会触发报错
照着报错信息下载nrf5_sdk_17.1.0_ddde560.zip(注意你购买的蓝牙适配器的芯片方案是否一致)
下载后解压到对应目录下即可
结果还是报错
于是换了一种思路
git clone https://github.com/greenlsi/nrf5-sdk |
然后再次执行cmake 成功
结果执行make的时候又报错了!!!! 崩溃了
在对应报错文件首行加入
from __future__ import print_function |
终于 经过了千辛万苦 我们得到了canokey_flash.uf2 canokey.hex两个文件
接下来就是烧录固件了
这里选择nrf connect https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-Desktop/Download#infotabs
安装后下载programmer
这里由于网络问题 导致直接在nrf connect中安装programmer失败
所以我们手动安装
cd C:\Users\22346\.nrfconnect-apps\local |
随后就可以成功利用programmer刷取固件了
直接把我们编译出来的canokey.hex拖入进去 点击write
接下来的部分是初始化
sudo apt-get install opensc pcscd pcsc-tools usbutils |
还有一个ccid 利用apt-get无法安装 在阿里云镜像站中找到了对应的安装包
https://mirrors.aliyun.com/ubuntu/pool/universe/c/ccid/?spm=a2c6h.25603864.0.0.25adc813rsLGW7
tar jxvf ccid_1.5.4.orig.tar.bz2 |
sudo apt install flex |
第二个报错
找到对应的包 https://launchpad.net/ubuntu/+source/pcsc-lite/2.0.1-1
tar jxvf pcsc-lite_2.0.1.orig.tar.bz2 pcsc-lite-2.0.1/ |
sudo apt install libsystemd-dev libudev-dev |
tar -xf polkit-121.tar.gz |
看了下说明文档 要用meson来编译安装
BUILD INSTRUCTIONS |
接下来开始编译
sudo apt install meson |
然后我们回到pcsc-lite的编译 可以成功运行下去了
回到ccid的编译 又又又遇到报错了
sudo apt install libusb-1.0-0-dev |
随后终于可以成功编译
接着将我们烧录好固件的蓝牙适配器接入电脑
使用lsusb查看是否检测到Clay Logic usb
(然后运行sudo pcsc_scan 发现还是一堆报错 忙活半天还是无法解决 最后还把虚拟机干坏了
于是选择更换了ubuntu22 编译ccid的时候几乎畅通无阻
重新运行lsusb 没有发现clay logic usb
点击vmware右下角的clay logic usb 点击和主机断开连接后
就可以检测到了
随后使用sudo pcsc_scan连接 切换另外一个终端
cd canokey-nrf52/utils |
抓包分析
这里我选择的设备是 E104-BT5032U 芯片采用的是nRF52832
首先安装wireshark 这里就不介绍了 注意要顺便装上usbPcap插件
根据产品提供的文档 下载对应的sniffer版本 https://www.ebyte.com/product-view-news.html?id=1745
将excap文件夹内的所有文件复制到wireshark的对应文件夹中
随后将Profile_nRF_Sniffer_Bluetooth_LE文件夹中的内容复制到wireshark的profiles中
随后将设备插入 此时应该能发现对应的接口
此外还需要打开 视图->接口工具栏->nRF sniffer for bluetooth
此举是为了便于我们选择设备
我们直接点击过滤器 此时接收到了大量的包 在刚刚打开的栏中 我们可以选择特定的设备接收
点开Device后 可以注意到前面的dBm
其是用来衡量蓝牙发送功率或者接收功率的对数单位
一个负数的dBm表示相对于一个毫瓦的参考功率而言的衰减
也就是说-80dBm的信号会强于-100dBm
由于抓包的地点选在了宿舍 为了减少其他学生的BLE设备带来的影响 这里在选用接口之前先对rssi进行限制
在开始接下来的操作之前 要明白所购买的E104-BT5032U到底是一个什么样的设备
在我刚开始 我一直误以为 是通过这个设备发送包来和主机通讯 然后我们使用wireshark来抓包
但是实际上 可以把其看作一个从设备 接收各个主设备的包 然后我们利用wireshark可以查看这些包
所以我们还需要在其他平台上对于wireshark所监听的设备 进行发包等操作
这样就可以通过wireshark来查看这些包的内容 从而达到我们更进一步分析的目的 至于想要搭建一个发包的设备 那就是后话了
这里我选择我的手机 REDMI K50PRO来进行抓包
这里先把理论知识来结合一下实际温习一遍
在上文的学习中 我们得知了一个设备包含多种服务 而服务包含多种特征 特征又包含多种属性
以上图举例 Generic Access就是一个服务 以及最后的Unknown Service也是
前者被识别出来了服务名字 说明其是一个官方定义的服务 而后者无法被识别说明是小米的私有服务 官方服务可以通过查询来获取其作用
https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.pdf?id=89
我们向手机发送请求 请求返回设备名称
来到wireshark 通过检索找到请求包以及返回包
先来看请求包 走的是物理通道
根据上文 其应该由前导码 访问地址 协议数据单元 循环冗余检验四个部分组成
图中黑框部分就是数据包 前面是nrf connect发包的一些参数
前导码是一个01交替的固定序列 在LE 1M PHY上为8bit
LE 2M PHY上为16bit 二者是不同的物理层传输方式 区别在于传输速率
通常为0x55或者是0xaa 但是这里我并没有找到前导码 位于包头的是访问地址
这里查阅了文档也没提及这一点 观察了其他的LL层报文 也都没有出现前导码
个人猜测是与我们使用nrf connect发包有关系 这个疑问得到后续的使用项目发包再来解决
访问地址用来指明A和B设备之间进行传输
但是你可以发现很多包的访问地址都是一样的 0x8e89bed6
此为广告物理通道数据包固定的访问地址
广告物理通道PDU仍然走的是物理通道 但是其是用于广播 扫描 发起连接
广告物理通道的PDU拥有16bit的标头
其拆分成各部分如上图所示
我们抓包得到的两个字节为 0xc30c
0x0c是length这部分 那么0xc3就是前面的五个部分
将其拆分成二进制可以得到
1 1 0 0 0011 |
观察wireshark解析出来的各个部分 和我们自己解析的不一样
联想到前面的访问地址在数据包中的形式 所以这里应该是采用了小端序存储
PDU TYPE 为0011 应对者SCAN_REQ 并且格式为LE 1M
接下来 显然红框的部分就是payload
一共是12字节 也印证了前面标头中的length没错
payload的内容定义了扫描设备的MAC地址以及广播设备的MAC地址
后面的三字节 即为crc校验码 用来确保数据传输过程中的完整性
bleak库的使用
接下来尝试使用python的bleak库来和蓝牙设备进行通讯 此举为了便于我们后面阅读蓝牙安全相关的论文或者是复现cve漏洞 https://github.com/hbldh/bleak
以下代码示例如非特殊说明 均在windows11平台下进行
同时 官方的代码示例中 会使用asyncio库进行异步编程 由于我的开发能力几乎没有 所以会借助这次机会 了解了解 下文中会偶尔穿插一些异步编程的理解
安装库
pip install bleak |
基础通讯实现
扫描
先来尝试扫描可连接的蓝牙设备
import asyncio |
可以看到扫描到了一些设备
前面的是设备的MAC地址 后面的是名称 虽然我们前面提到 设备的设备地址是唯一的
其中设备地址有两种形式 公共设备地址以及随机设备地址
公共设备地址是需要向IEEE申请的 同时需要花钱 但具有唯一性
而随机设备地址在设备启动后随机生成
在现如今不断增加的蓝牙设备 尤其是大量的BLE设备 显然前者的弊端是比较大的 所以大多数采用的是随机设备地址
这里以上图中的蓝牙键盘 Kzzi-K75为例 在第一次扫描的过程中 其MAC地址为DD:BB:CF:F4:BD:D9
接下来我们关闭该设备 随后再次打开 再次扫描一下试试
可以发现此时变更为了F1:4E:33:18:EC:D5
接下来我们尝试使用python脚本来实现和上面用nrf connect工具获取设备名称的操作
async def get_devideinfo(address): |
这里使用BleakClient构造函数 以address为参数 创建了一个client对象
client此时是一个客户端实例 可以将其理解为一个主设备 用于连接其他设备 以及发起通讯
通过上述代码 我们可以获取到address对应的设备的服务及其特征
还可以通过输出c.properties来查看特征值的权限
读取和写入
接下来 我们将对特征值动手 学习使用write_gatt_char()和read_gatt_char()两个方法
我们前面提到过ATT层的核心就是属性
属性由 句柄 类型 值 权限这四个部分组成
句柄是一个2字节的十六进制码 在系统初始化时为0x0001 逐步增加1 最高到0xffff
可以把其看作一个指向属性的指针 通过句柄可以访问到对应的属性
类型是用来区分当前属性是服务还是特征 具体的表现形式是uuid
Ble的属性类型一共有四大类
Primary Service(首要服务项) |
uuid中的第三和第四字节对于属性类型进行了标注
以下列这个uuid为例 00002a00-0000-1000-8000-00805f9b34fb
其第三第四字节为0x2a00
根据下列表格判断得到 为特征值类型
0x1800 – 0x26FF :服务项类型 |
值则用来存放属性的内容 其需要一定的内存空间 操控属性值就是对这块内容空间进行更改
属性权限包括四种 访问权限 加密权限 认证权限 授权权限
了解了这些前置知识 再来观摩观摩官方给出的几个bleak库例子代码
https://github.com/hbldh/bleak/blob/develop/examples/sensortag.py
来看68行
uuid16_lookup = {v: normalize_uuid_16(k) for k, v in uuid16_dict.items()} |
漏洞复现
CVE-2017-0785
前言
该漏洞的核心在于BlueDroid和Fluoride这两个适用于安卓操作系统的蓝牙协议栈
总体的理解难度比较大 包含了比较多的新知识
背景普及
我们上文中仅对于低功耗蓝牙协议栈进行了普及 但是我们本次复现的漏洞是关于经典蓝牙的 所以这里先对于漏洞的发生层SDP 进行一些简单的介绍以及了解经典蓝牙的协议栈
上图中 红色部分是经典蓝牙协议栈独有的 绿色部分是低功耗蓝牙所独有的 蓝色部分为公共部分
但是经典蓝牙也可以拥有部分绿色的特性
SDP层为服务发现协议(SERVICE DISCOVERY PROTOCOL) 为应用程序提供方法发现哪些服务可用 并确认这些服务的特征
你是否还记得前面学习的抓包 我们抓取的是利用nrf connect for moblie来向ble设备发包
通过wireshark抓取到的就是一个request pdu和一个response pdu
sdp也是同理 其实际上就是定义一个client如何找到server提供的服务
client先向server发送一个request pdu 等待server相应后返回response pdu
来观察一下两种pdu的构造
Bluetooth SDP Protocol |
先是request pdu 有两个比较重要的参数Maximum Attribute Byte Count和Continuation State
前者用于限制response pdu返回的数据大小
比如这里为65535 即不得超过对应字节 来结合下面这个response pdu
Bluetooth SDP Protocol |
Attribute List Byte Count标识了response pdu包含的数据大小 为662
Attribute List Byte Count的大小不得超过Maximum Attribute Byte Count所定义的
如果超过了 就会对response pdu进行分段 随后利用Continuation State进行传输
这里的分段大小是由server自己决定的 比如上述的两个pdu Attribute List Byte Count就是662 并没有将Maximum Attribute Byte Count全部分割
目前手头还没有设备能够抓取sdp层的包 所以这里先使用其他文章中的包来分析
Continuation State包含两个部分 InfoLength和Continuation Information
前者用来标识后者的长度 最大值为0x10
后者通过response pdu发回给client 随后client会将其放到下一个request pdu中 发送给server 从而再作出分段的决策 以此来往复
但是在实际的过程中 造成分段的并不只有上述这一个原因
L2CAP的Maximum Transmission Unit参数仍然会限制数据大小 从而造成分段
SDP层详解
代码分析
需要进一步了解Continuation Information在Android中是如何定义的 就需要我们来阅读源码
https://android.googlesource.com/platform/system/bt/+/refs/tags/android-8.0.0_r1/stack/sdp/sdpint.h 中的第200行
#if (SDP_SERVER_ENABLED == TRUE) |
其中的 cont_offset起到了关键的作用 从注释中可以得知 其是用来指示客户端在请求继续传输时 从响应数据的哪个位置开始获取后续的数据
也就是我们上文说到的数据分段的时候 其负责给server索引分段的数据
根据pdu的不同 可以分为两种情况
第一类情形是使用SDP_SERVICE_SEARCH_REQ/RSP PDU时 即服务搜索请求和服务搜索响应
此时的数据单位称之为数据项 如果上一个pdu已经传输了20数据项 那么cont_offset的值就是20
第二类情形是使用SDP_SERVICE_ATTR_REQ/RSP 或 SDP_SERVICE_SEARCH_ATTR_REQ/RSP 时 即服务属性请求/响应 服务属性搜索请求/响应
区别于前者 数据的单位由数据项变成了字节 其代表的是已经传输的数据总和 比如如果为662 说明前面传输的分段数据大小为662字节
总是纸上谈兵可不能理解清楚 我们来尝试利用wireshark抓一个sdp包来看看