BLE GATT / ATT 抓包实战
BLE GATT / ATT 基础 —— 属性、Handle、Characteristic 与 ATT 协议
- 一、GATT 是什么
- 1.1 GATT 站在哪一层
- 1.2 GATT 和 ATT 的关系
- 1.3 属性(Attribute)—— GATT 的最小单位
- 1.4 Handle(句柄)—— GATT 的"地址"
- 1.5 UUID —— 属性的"类型标识"
- 1.6 常见标准 UUID 速查
- 1.7 GATT 层级结构
- 二、Characteristic 的完整解剖
- 2.1 一个 Characteristic 至少占 2 个属性
- 2.2 Properties 位域
- 2.3 Notification vs Indication
- 2.4 CCCD(0x2902)—— 通知开关
- 三、ATT 协议
- 3.1 ATT 在协议栈的位置
- 3.2 ATT 的 6 种 PDU 类型
- 3.3 ATT PDU 的通用格式
- 3.4 PDU 详细 opcode 速查
- 3.4.1 Requests(需要 Response)
- 3.4.2 Responses
- 3.4.3 Commands(不需要 Response)
- 3.4.4 Notifications
- 3.4.5 Indications
- 3.4.6 Confirmations
- 3.5 ATT_MTU
- 3.6 ATT 错误处理:Error Response(0x01)
一、GATT 是什么
1.1 GATT 站在哪一层
应用层(心率应用) ↑ ( GATT Generic Attribute Profile ) ← 数据组织规范 ↑ 基于 ( ATT Attribute Protocol ) ← 传输协议,本文重点 ↑ 基于 L2CAP(逻辑链路控制与适配协议,CID = 0x0004) ↑ 基于 Link Layer(链路层) ↑ 基于 Physical Layer(1M / 2M / Coded PHY)记住这条链:
GATT 基于 ATT,ATT 跑在 L2CAP CID 0x0004 上,L2CAP 跑在链路层上。 抓包时从上往下解,每一层都有自己的头部。
1.2 GATT 和 ATT 的关系
| 协议 | 角色 | 类比 |
|---|---|---|
| ATT | 传输协议:定义"怎么读写数据" | 像 SQL 语言(SELECT/UPDATE) |
| GATT | 框架/规范:定义"数据怎么组织" | 像数据库表结构规范(有哪些表、字段) |
1.3 属性(Attribute)—— GATT 的最小单位
一个属性包含4 个部分:
| 组成部分 | 说明 | 例子 |
|---|---|---|
| Attribute Handle | 属性的 “门牌号”(16-bit) | 0x000d |
| Attribute Type | 属性的类型(用 UUID 表示) | 0x2a01(Appearance) |
| Attribute Value | 属性的实际值 | 0x0341(心率带) |
| Attribute Permissions | 读/写权限 | Read Only / Read+Write |
⚠️ 注意:Permissions 不出现在空口上,它是服务端内部的安全策略。空口上客户端只能看到 Properties(见第 2.2 节)。
1.4 Handle(句柄)—— GATT 的"地址"
为什么需要 Handle?为什么不直接用 UUID 来读写数据?
因为:同一个 UUID 可能出现多次,但 Handle 是全局唯一的。
比如一个设备可能有 3 个温度传感器特征,UUID 都是0x2A6E,但 handle 分别是0x0012、0x0018、0x0024。
Handle 的特点:
| 特点 | 说明 |
|---|---|
| 16-bit | 范围0x0001~0xFFFF |
| 服务器分配 | GATT Server(板子)在建库时分配 |
| 单调递增 | 通常按声明顺序递增 |
| 客户端靠它操作 | 所有 ATT 请求都填 handle |
| UUID 靠它发现 | 客户端先通过发现流程知道"handle X 是 UUID Y" |
核心结论:所有 ATT 操作(读/写/通知)都是针对 handle 的,不是针对 UUID。
1.5 UUID —— 属性的"类型标识"
两种长度:
| 类型 | 长度 | 格式 | 用途 |
|---|---|---|---|
| 16-bit UUID | 2 字节 | 0x180D | 蓝牙 SIG 定义的标准类型 |
| 128-bit UUID | 16 字节 | 0000180D-0000-1000-8000-00805F9B34FB | 厂商自定义 |
转换规则:蓝牙 SIG 定义了一个基础 UUID(Base UUID)
00000000-0000-1000-8000-00805F9B34FB ↑ 这 16 位用 16-bit UUID 替换所以:
0x180D → 0000180D-0000-1000-8000-00805F9B34FB 0x2A37 → 00002A37-0000-1000-8000-00805F9B34FB💡 因为 16-bit UUID 都能无损展开成 128-bit,所以协议里传输 16-bit UUID 只是省带宽,两者语义完全等价。
1.6 常见标准 UUID 速查
服务(Service)
| UUID | 名称 |
|---|---|
| 0x1800 | Generic Access(GAP) |
| 0x1801 | Generic Attribute(GATT) |
| 0x180A | Device Information |
| 0x180D | Heart Rate⭐ |
| 0x180F | Battery⭐ |
| 0x1812 | Human Interface Device(HID) |
| 0x110A | Audio Source |
特征(Characteristic)
| UUID | 名称 |
|---|---|
| 0x2A00 | Device Name |
| 0x2A01 | Appearance(外观) |
| 0x2A04 | Peripheral Preferred Connection Parameters(PPCP) |
| 0x2A19 | Battery Level⭐ |
| 0x2A37 | Heart Rate Measurement⭐ |
| 0x2A38 | Body Sensor Location |
| 0x2A39 | Heart Rate Control Point |
| 0x2A29 | Manufacturer Name |
| 0x2A24 | Model Number |
声明/描述符(Declaration / Descriptor)
| UUID | 名称 | 作用 |
|---|---|---|
| 0x2800 | Primary Service | 声明"这是一个主服务" |
| 0x2801 | Secondary Service | 声明次服务 |
| 0x2802 | Include | 引用其他服务 |
| 0x2803 | Characteristic | 声明"这是一个特征" |
| 0x2902 | Client Characteristic Configuration | CCCD,控制通知开关⭐ |
| 0x2901 | Characteristic User Description | 人类可读描述 |
| 0x2900 | Characteristic Extended Properties | 扩展属性 |
| 0x2904 | Characteristic Presentation Format | 数据格式(单位、指数等) |
1.7 GATT 层级结构
Profile (规范,非协议实体) │ ├── Service 1 (服务,如心率服务 0x180D) │ │ │ ├── Characteristic 1 (特征,如心率测量 0x2A37) │ │ ├── Declaration(声明,UUID 0x2803) │ │ ├── Value(值,UUID 0x2A37) │ │ └── Descriptor(描述符,如 CCCD 0x2902) │ │ │ └── Characteristic 2 (如 Body Sensor Location 0x2A38) │ ├── Declaration(0x2803) │ └── Value(0x2A38) │ └── Service 2 (如电池服务 0x180F) │ └── Characteristic Battery Level(0x2A19) ├── Declaration(0x2803) ├── Value(0x2A19) └── CCCD(0x2902)| 概念 | 说明 | 是否协议实体 |
|---|---|---|
| Profile | 一组服务的 “规范/约定”(如 “心率 Profile” 规定了必须有 HRS + DIS) | ❌ 概念,不是实体 |
| Service | 一组相关特征的集合 | ✅ 实体 |
| Characteristic | 数据的基本单位 | ✅ 实体 |
| Descriptor | 特征的附加元数据 | ✅ 实体 |
二、Characteristic 的完整解剖
2.1 一个 Characteristic 至少占 2 个属性
一个 Characteristic 在 GATT 表里占用至少 2 个属性(Attribute):
属性 1:Characteristic Declaration(特征声明)
UUID = 0x2803,Value 包含 3 个部分:
| 字段 | 长度 | 说明 |
|---|---|---|
| Properties | 1 字节 | 读/写/通知权限(位域,见 2.3) |
| Value Handle | 2 字节 | 特征值的 handle(指向下一个属性) |
| Characteristic UUID | 2 或 16 字节 | 特征的 UUID |
2.1.2 属性 2:Characteristic Value(特征值)
UUID= 特征本身的 UUID(如0x2A37)Value= 实际数据
属性 3+:Descriptors(可选)
- 如 CCCD(
0x2902)。
2.2 Properties 位域
Properties 是1 字节的位域,定义在 Characteristic Declaration 里:
| Bit | 名称 | 含义 |
|---|---|---|
| 0 | Broadcast | 允许广播该特征值 |
| 1 | Read | 允许读 |
| 2 | Write Without Response | 允许写,不需要回应 |
| 3 | Write | 允许写,需要回应 |
| 4 | Notify | 允许通知(服务端主动发,无确认) |
| 5 | Indicate | 允许指示(服务端主动发,需确认) |
| 6 | Authenticated Signed Writes | 允许签名写 |
| 7 | Extended Properties | 有扩展属性 |
速记常数值:
| Properties 值 | 拆解 | 含义 |
|---|---|---|
0x02 | bit1 | Read |
0x08 | bit3 | Write |
0x0A | bit1 + bit3 | Read + Write |
0x10 | bit4 | Notify |
0x12 | bit1 + bit4 | Read + Notify |
0x20 | bit5 | Indicate |
2.3 Notification vs Indication
这是 GATT 数据上报的两种方式:
| 对比 | Notification | Indication |
|---|---|---|
| Opcode | 0x1B(Handle Value Notification) | 0x1D(Handle Value Indication) |
| 方向 | Server → Client | Server → Client |
| 应用层确认? | ❌ 不需要 | ✅ 需要(Client 回0x1EConfirmation) |
| 链路层 ACK | ✅ 有 | ✅ 有 |
| 可靠性 | 较低(可能丢) | 较高(有确认) |
| 速率 | 快(可连发) | 慢(要等确认) |
| 触发条件 | 写 CCCD =0x0001 | 写 CCCD =0x0002 |
| 典型用途 | 传感器数据(心率、温度) | 关键告警、状态变更 |
Notification 的 “不需要确认” 是指应用层不确认,但链路层仍然有 ACK 和重传机制。 所以 Notification 在链路层是可靠的,只是应用层不知道对方有没有收到。
2.4 CCCD(0x2902)—— 通知开关
CCCD =Client Characteristic Configuration Descriptor,是客户端用来控制服务端是否发通知的开关。
| 值 | 含义 |
|---|---|
0x0000 | 关闭通知和指示 |
0x0001 | 使能Notification |
0x0002 | 使能Indication |
0x0003 | 同时使能(很少用) |
为什么需要 CCCD?
因为服务端(板子)不知道客户端(手机)想不想收通知。所以:
- 手机连接后,写 CCCD =
0x0001告诉板子:“我要收通知” - 板子收到后,才在特征值变化时发 Notification
- 如果手机不写,板子不会主动发任何通知
三、ATT 协议
3.1 ATT 在协议栈的位置
GATT(数据组织规范:Service / Characteristic / Descriptor) ↑ 基于 ATT(传输协议:Read / Write / Notify / Indicate) ← 这一层 ↑ 基于 L2CAP(CID = 0x0004,固定给 ATT 用) ↑ 基于 Link LayerATT 占用的 L2CAP 通道是固定的
CID = 0x0004。
3.2 ATT 的 6 种 PDU 类型
| 类型 | 方向 | 需要回应? | 典型 opcode |
|---|---|---|---|
| Request | Client → Server | ✅ 必须等 Response | 0x08Read By Type Req |
| Response | Server → Client | —(本身就是回应) | 0x09Read By Type Rsp |
| Command | Client → Server | ❌ 不等 | 0x52Write Command |
| Notification | Server → Client | ❌ 不确认 | 0x1B |
| Indication | Server → Client | ✅ 需 Confirmation | 0x1D |
| Confirmation | Client → Server | —(回应 Indication) | 0x1E |
3.3 ATT PDU 的通用格式
┌──────────┬─────────────────────────┐ │ Opcode │ Parameters(参数) │ │ 1 byte │ 长度取决于 opcode │ └──────────┴─────────────────────────┘Opcode 的位结构(严格定义)
Bit: 7 6 5 4 3 2 1 0 ┌────────┬────────┬──────────────────────────┐ │AuthSig │Command │ Method │ │ Flag │ Flag │ (6 bits) │ └────────┴────────┴──────────────────────────┘| 位 | 名称 | 含义 |
|---|---|---|
| Bit 7 | Auth Sig Flag | 1 = 该 PDU 带认证签名 |
| Bit 6 | Command Flag | 1 = Command(不需回应);0 = Request(需回应) |
| Bit 0~5 | Method | 方法编号(0~63),决定 “做什么操作” |
💡这个位结构解释了那些 “奇怪的” opcode 值:
所有 Write 操作的Method 字段都是0x12,区别只在上面两个 flag 位:
| 完整 Opcode | 二进制 | bit7 AuthSig | bit6 Command | Method | 名称 |
|---|---|---|---|---|---|
0x12 | 0001 0010 | 0 | 0 | 0x12 | WriteRequest(要回应) |
0x52 | 0101 0010 | 0 | 1 | 0x12 | WriteCommand(不回应) |
0xD2 | 1101 0010 | 1 | 1 | 0x12 | SignedWrite Command(带签名) |
3.4 PDU 详细 opcode 速查
3.4.1 Requests(需要 Response)
| Opcode | 名称 | 作用 |
|---|---|---|
| 0x02 | Exchange MTU Request | 协商 MTU |
| 0x04 | Find Information Request | 查某 handle 的类型 |
| 0x08 | Read By Type Request | 按 UUID 查属性 |
| 0x0A | Read Request | 按 handle 读值 |
| 0x10 | Read By Group Type Request | 按 UUID 查一组(服务发现) |
| 0x12 | Write Request | 写值(要回应) |
| 0x16 | Prepare Write Request | 准备写(长数据分块) |
| 0x18 | Execute Write Request | 执行写(提交分块) |
特点:Client 发完必须等 Response,期间不能发第二个 Request(串行)。
3.4.2 Responses
| Opcode | 名称 |
|---|---|
| 0x01 | Error Response(任何请求失败都回这个) |
| 0x03 | Exchange MTU Response |
| 0x05 | Find Information Response |
| 0x09 | Read By Type Response |
| 0x0B | Read Response |
| 0x11 | Read By Group Type Response |
| 0x13 | Write Response |
3.4.3 Commands(不需要 Response)
| Opcode | 名称 | 用途 |
|---|---|---|
| 0x52 | Write Command | 快速写,不等回应(如串口透传 TX) |
| 0xD2 | Signed Write Command | 带签名的写 |
特点:Client 发完就走,可以连发多个(流控靠链路层)。
3.4.4 Notifications
| Opcode | 名称 |
|---|---|
| 0x1B | Handle Value Notification |
Server 主动发,Client 不回任何确认。
3.4.5 Indications
| Opcode | 名称 |
|---|---|
| 0x1D | Handle Value Indication |
Server 主动发,Client 必须回 Confirmation。
3.4.6 Confirmations
| Opcode | 名称 |
|---|---|
| 0x1E | Handle Value Confirmation |
Client 回应 Indication。
3.5 ATT_MTU
ATT_MTU = ATT 层一次能传输的最大字节数。
| 项 | 值 |
|---|---|
| 默认 ATT_MTU | 23 字节 |
| 最大 ATT_MTU | 517 字节 |
| 协商方式 | Exchange MTU Request(0x02)/ Response(0x03) |
为什么默认是 23?
链路层 payload 默认 27 字节 - L2CAP 头 4 字节 = 23 字节 ← ATT_MTU 默认值这就是 ATT_MTU 和 DLE 的绑定关系:想让 ATT_MTU 变大,必须先把链路层的 DLE 协商上去。否则链路层一次只能装 27 字节,MTU 再大也没用。
MTU 协商流程
Client → Server: Exchange MTU Request (Client RX MTU = 247) Server → Client: Exchange MTU Response (Server RX MTU = 65) ──────────────────────────────────────────────────────── 最终 ATT_MTU = min(247, 65) = 65和 DLE 一样,也是取min。
MTU 对实际数据的影响
Notification 的 PDU 格式:
[1 byte opcode][2 bytes handle][Value...]所以实际能传的数据 = ATT_MTU - 3:
| ATT_MTU | Notification 最大数据 |
|---|---|
| 23 | 20 字节 |
| 65 | 62 字节 |
| 247 | 244 字节 |
| 517 | 514 字节 |
📌 这解释了那个经典数字:BLE 4.0/4.1 时代一次通知最多只能带 20 字节有效数据(23 - 3)。
3.6 ATT 错误处理:Error Response(0x01)
当任何 Request 失败时,Server 返回 Error Response:
┌────────┬─────────────────────────┬───────────────────────────┬─────────────┐ │ Opcode │ Request Opcode In Error │ Attribute Handle In Error │ Error Code │ │ 1 byte │ 1 byte │ 2 bytes │ 1 byte │ └────────┴─────────────────────────┴───────────────────────────┴─────────────┘| 字段 | 含义 |
|---|---|
| Request Opcode In Error | 哪个请求出错了 |
| Attribute Handle In Error | 哪个 handle 出问题 |
| Error Code | 错误原因 |
常见错误码
| Code | 名称 | 含义 |
|---|---|---|
| 0x01 | Invalid Handle | handle 不存在 |
| 0x02 | Read Not Permitted | 不允许读 |
| 0x03 | Write Not Permitted | 不允许写 |
| 0x05 | Insufficient Authentication | 需要认证(配对) |
| 0x06 | Request Not Supported | 不支持该请求 |
| 0x07 | Invalid Offset | 偏移错误 |
| 0x08 | Insufficient Authorization | 需要授权 |
| 0x0A | Attribute Not Found | 找不到属性 |
| 0x0C | Insufficient Encryption Key Size | 密钥长度不够 |
| 0x0D | Invalid Attribute Value Length | 值长度错误 |
★ 0x0A Attribute Not Found 的特殊用途
服务发现就是靠这个错误码 "结束"的!
Client: Read By Group Type Request (handle 0x0001~0xffff) Server: Read By Group Type Response (服务 1, 2, 3) Client: Read By Group Type Request (handle 0x0015~0xffff) ← 从下一个 handle 继续 Server: Read By Group Type Response (服务 4, 5) Client: Read By Group Type Request (handle 0x0020~0xffff) Server: Error Response (0x0A Attribute Not Found) ← 没有了!收到0x0A,客户端就知道 “所有服务都发现完了”。
这是一个非常巧妙的设计 —— 用错误码表示 “迭代结束”。所以抓包里看到
0x0A不要慌,在发现流程里它是正常的终止信号,不是故障。