应用开发者可基于 BLE 提供的 ATT/GATT
协议,开发自己的私有协议。就像我们可以基于 TCP 创建 HTTP 协议一样。同样道理,对于经典蓝牙,厂家也可以在 L2CAP/RFCOMM
协议的基础上,封装自己的私有协议,实现额外的消息加密、身份认证等。 我们在上一篇文章 使用 Python 模块 bluepy 玩转 BLE 说过低功耗蓝牙应用层协议的测试方法。本次就来聊聊经典蓝牙(BR,Basic Rate)一些私有协议的测试方法。
01 Python 脚本实现 RFCOMM 数据包收发
python 提供的蓝牙模块
- pybluez: Python code to access the host machine’s Bluetooth resources. 用于经典蓝牙
- bluepy: Python interface to Bluetooth LE on Linux. 用于低功耗蓝牙
Linux 启用蓝牙适配器
插入蓝牙适配器,hciconfig
命令可以查看当前 Linux 存在的 HCI 设备
1.当前设备默认关闭,开启蓝牙
$ sudo hciconfig hci1 up
2.扫描周围蓝牙设备
└─$ hcitool -i hci1 scan
Scanning ...
50:E0:85:XX:XX:XX n/a
80:CF:A2:XX:XX:XX n/a
A4:83:E7:XX:XX:XX LZX
3.扫描目标蓝牙设备支持的服务
$ sdptool browse A4:83:E7:XX:XX:XX
简单了解蓝牙渗透的基本命令之后,我们使用脚本实现上述命令
PyBluez 实现经典蓝牙收发
安装 pybluez(建议使用 Linux,Windows 可能会出现奇怪的问题)
pip install pybluez2 # > python3.6
pip install pybluez # < python3.6
pybluez 官方提供了几个使用案例,我们稍稍整合一下,扫描当前经典蓝牙设备
import bluetooth
print("Performing inquiry...")
nearby_devices = bluetooth.discover_devices(duration=8, lookup_names=True,
flush_cache=True, lookup_class=False)
print("Found {} devices".format(len(nearby_devices)))
for addr, name in nearby_devices:
try:
print(" {} - {}".format(addr, name))
except UnicodeEncodeError:
print(" {} - {}".format(addr, name.encode("utf-8", "replace")))
发现的设备
扫描特定设备支持的服务
import sys
import bluetooth
if len(sys.argv) < 2:
print("Usage: sdp-browse.py <addr>")
print(" addr - can be a bluetooth address, \"localhost\", or \"all\"")
sys.exit(2)
target = sys.argv[1]
if target == "all":
target = None
services = bluetooth.find_service(address=target)
if len(services) > 0:
print("Found {} services on {}.".format(len(services), sys.argv[1]))
else:
print("No services found.")
for svc in services:
print("\nService Name:", svc["name"])
print(" Host: ", svc["host"])
print(" Description:", svc["description"])
print(" Provided By:", svc["provider"])
print(" Protocol: ", svc["protocol"])
print(" channel/PSM:", svc["port"])
print(" svc classes:", svc["service-classes"])
print(" profiles: ", svc["profiles"])
print(" service id: ", svc["service-id"])
发送 L2CAP/RFCOMM 协议报文
socket = bluetooth.BluetoothSocket(bluetooth.L2CAP)
socket.connect(("aa:bb:cc:dd:ee:ff", 23)) # Channel:23
socket.send(b"\x00\x01\x02") # 可以定制化 Fuzzing
socket.recv(100)
02 Android App 实现 RFCOMM 数据包收发
Android SDK 提供了较为丰富的蓝牙 API,我们仍然以 RFCOMM 协议为例,因为很多私有协议建立在这层协议基础上。主要代码如下
private static final String uuid = "82ff3820-8411-400c-b85a-ffbdb32cf0ff";
public void connectBt() {
try {
/* 获取本地蓝牙适配器 */
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
/* 打开蓝牙 */
if (!mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.isEnabled();
}
/* 获取当前已绑定的设备 */
Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
Log.i(TAG, "device nubmer: " + devices.size());
for (BluetoothDevice device:devices) {
Log.i(TAG, "name: " + device.getName() + "\naddress: " + device.getAddress());
/* 与已绑定设备建立RFCOMM连接 */
if (device.getAddress().equals("94:08:C7:aa:bb:cc")) {
try {
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
socket.connect();
if (socket != null) {
Log.i(TAG, "+++++ Success to create RFCOMM connection +++++");
/* 在新的线程里实现异步读 */
MainActivity.this.brThread = new BrThread(socket);
MainActivity.this.brThread.start();
/* 在当前线程里实现发送数据 */
/* 将brThread全局化是为了方便同时使用其他方法发送bt-rfcomm数据包 */
MainActivity.this.brThread.write(Helper.string_byte("001122334455"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (SecurityException e) {
e.printStackTrace();
}
}
发送蓝牙 RFCOMM 消息简单,但是接收消息需要单独启用线程完成
package com.lys.brtest;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import com.lys.brtest.util.Helper;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
public class BrThread extends Thread{
private static final String TAG = "BrThread";
private BluetoothSocket btSocket;
private InputStream mmInStream;
private OutputStream mmOutStream;
public BrThread(BluetoothSocket socket) {
btSocket = socket;
try {
mmInStream = btSocket.getInputStream();
mmOutStream = btSocket.getOutputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void run() {
// 监听输入流
while (true) {
try {
byte[] buffer = new byte[1024];
// 读取输入流
int bytes = mmInStream.read(buffer);
Log.i(TAG, String.format("[%d] Recv <========================\n%s", bytes, Helper.byte_string(Arrays.copyOfRange(buffer, 0, bytes))));
} catch (IOException e) {
Log.e(TAG, "+++++ Disconnected +++++", e);
break;
}
}
}
public void write(byte[] bytes) {
// 发送数据
Log.i(TAG, String.format("[%d] Send >========================\n%s", bytes.length, Helper.byte_string(bytes)));
try {
if (mmOutStream != null) {
mmOutStream.write(bytes);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
当然,还需要一些额外的方法完成字节数组与十六进制字符串的转换
public class Helper {
// 字节数组转十六进制
public static String byte_string(byte[] data) {
StringBuilder sb = new StringBuilder();
for (byte b : data) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// 十六进制转字节数组
public static byte[] string_byte(String hexString) {
hexString = hexString.replaceAll(" ", "");
int len = hexString.length();
byte[] bytes = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
// 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节
bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
.digit(hexString.charAt(i+1), 16));
}
return bytes;
}
// 随机十六进制
public static String get_random_hex(int len, int range) {
Random random = new Random();
if (len == 0) len = random.nextInt(256);
byte[] data = new byte[len];
for (int i = 0; i < len; i++) {
if (range == 0)
// -128 ~ 127
data[i] = (byte) (random.nextInt(0xff) + (-128));
else
data[i] = (byte) (random.nextInt(range));
}
return Helper.byte_string(data);
}
}
如果遇到 CRC 校验,请注意,CRC 校验算法有很多,不同算法计算结果并不一致,Android-BLE-Common-Library 项目提供蓝牙领域经常使用的 CRC 校验函数。
# 某私有协议格式化
| service Id | feture Id | TLV |
这个时候就可以按照协议格式进行 Fuzzing 测试了。可以通过目标设备是否下线,或者蓝牙连接是否断开作为反馈。
也有一些 App 已经集成 RFComm 协议测试,例如 Android RFComm Emulator,蓝牙调试宝,这些应用可以用来收发包,可以用来调试,但是不支持二次开发,所以最好还是自己编写收发包 APK。
03 其他思路
除了上面提到的通过蓝牙发包方式进行 Fuzzing,可以考虑将私有协议解析部分的代码单独解耦,编译,利用 AFL/Libfuzzer 进行有路径反馈的 Fuzzing;在没有源代码的情况下,还可以 patch 协议解析部分的二进制,使用 AFL-qemu 进行 Fuzzing。